commit c2d68ee68c023bdbba938076fda31176a349d448 Author: mark H Date: Mon Jan 13 18:00:03 2025 +0800 up diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..0f673d1 --- /dev/null +++ b/README.rst @@ -0,0 +1,247 @@ +============================= +Authentication OpenID Connect +============================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:cd754fc72d2039d02ab1b8aec98af43fb9543c9a70f2150ab6e482954e4e83d6 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--auth-lightgray.png?logo=github + :target: https://github.com/OCA/server-auth/tree/18.0/auth_oidc + :alt: OCA/server-auth +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-auth-18-0/server-auth-18-0-auth_oidc + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/server-auth&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows users to login through an OpenID Connect provider +using the authorization code flow or implicit flow. + +Note the implicit flow is not recommended because it exposes access +tokens to the browser and in http logs. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +This module depends on the +`python-jose `__ library, not to +be confused with ``jose`` which is also available on PyPI. + +Configuration +============= + +Setup for Microsoft Azure +------------------------- + +Example configuration with OpenID Connect authorization code flow. + +1. configure a new web application in Azure with OpenID and code flow + (see the `provider + documentation `__)) + +2. in this application the redirect url must be be "/auth_oauth/signin" and of course this URL should be reachable + from Azure + +3. create a new authentication provider in Odoo with the following + parameters (see the `portal + documentation `__ + for more information): + +|image| + +|image1| + +Single tenant provider limits the access to user of your tenant, while +Multitenants allow access for all AzureAD users, so user of foreign +companies can use their AzureAD login without an guest account. + +- Provider Name: Azure AD Single Tenant +- Client ID: Application (client) id +- Client Secret: Client secret +- Allowed: yes + +or + +- Provider Name: Azure AD Multitenant +- Client ID: Application (client) id +- Client Secret: Client secret +- Allowed: yes +- replace {tenant_id} in urls with your Azure tenant id + +|image2| + +Setup for Keycloak +------------------ + +Example configuration with OpenID Connect authorization code flow. + +In Keycloak: + +1. configure a new Client +2. make sure Authorization Code Flow is Enabled. +3. configure the client Access Type as "confidential" and take note of + the client secret in the Credentials tab +4. configure the redirect url to be "/auth_oauth/signin" + +In Odoo, create a new Oauth Provider with the following parameters: + +- Provider name: Keycloak (or any name you like that identify your + keycloak provider) +- Auth Flow: OpenID Connect (authorization code flow) +- Client ID: the same Client ID you entered when configuring the client + in Keycloak +- Client Secret: found in keycloak on the client Credentials tab +- Allowed: yes +- Body: the link text to appear on the login page, such as Login with + Keycloak +- Scope: openid email +- Authentication URL: The "authorization_endpoint" URL found in the + OpenID Endpoint Configuration of your Keycloak realm +- Token URL: The "token_endpoint" URL found in the OpenID Endpoint + Configuration of your Keycloak realm +- JWKS URL: The "jwks_uri" URL found in the OpenID Endpoint + Configuration of your Keycloak realm + +.. |image| image:: https://raw.githubusercontent.com/OCA/server-auth/18.0/auth_oidc/static/description/oauth-microsoft_azure-api_permissions.png +.. |image1| image:: https://raw.githubusercontent.com/OCA/server-auth/18.0/auth_oidc/static/description/oauth-microsoft_azure-optional_claims.png +.. |image2| image:: https://raw.githubusercontent.com/OCA/server-auth/18.0/auth_oidc/static/description/odoo-azure_ad_multitenant.png + +Usage +===== + +On the login page, click on the authentication provider you configured. + +Known issues / Roadmap +====================== + +- When going to the login screen, check for a existing token and do a + direct login without the clicking on the SSO link +- When doing a logout an extra option to also logout at the SSO + provider. + +Changelog +========= + +18.0.1.0.0 2024-10-09 +--------------------- + +- Odoo 18 migration + +17.0.1.0.0 2024-03-20 +--------------------- + +- Odoo 17 migration + +16.0.1.1.0 2024-02-28 +--------------------- + +- Forward port OpenID Connect fixes from 15.0 to 16.0 + +16.0.1.0.2 2023-11-16 +--------------------- + +- Readme link updates + +16.0.1.0.1 2023-10-09 +--------------------- + +- Add AzureAD code flow provider + +16.0.1.0.0 2023-01-27 +--------------------- + +- Odoo 16 migration + +15.0.1.0.0 2023-01-06 +--------------------- + +- Odoo 15 migration + +14.0.1.0.0 2021-12-10 +--------------------- + +- Odoo 14 migration + +13.0.1.0.0 2020-04-10 +--------------------- + +- Odoo 13 migration, add authorization code flow. + +10.0.1.0.0 2018-10-05 +--------------------- + +- Initial implementation + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* ICTSTUDIO +* André Schenkels +* ACSONE SA/NV + +Contributors +------------ + +- Alexandre Fayolle +- Stéphane Bidoul +- David Jaen +- Andreas Perhab + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-sbidoul| image:: https://github.com/sbidoul.png?size=40px + :target: https://github.com/sbidoul + :alt: sbidoul + +Current `maintainer `__: + +|maintainer-sbidoul| + +This module is part of the `OCA/server-auth `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..8b36099 --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2016 ICTSTUDIO +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import controllers +from . import models diff --git a/__manifest__.py b/__manifest__.py new file mode 100644 index 0000000..f6897d2 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2016 ICTSTUDIO +# Copyright 2021 ACSONE SA/NV +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Authentication OpenID Connect", + "version": "18.0.1.0.0", + "license": "AGPL-3", + "author": ( + "ICTSTUDIO, André Schenkels, " + "ACSONE SA/NV, " + "Odoo Community Association (OCA)" + ), + "maintainers": ["sbidoul"], + "website": "https://github.com/OCA/server-auth", + "summary": "Allow users to login through OpenID Connect Provider", + "external_dependencies": {"python": ["python-jose"]}, + "depends": ["auth_oauth"], + "data": ["views/auth_oauth_provider.xml", "data/auth_oauth_data.xml"], + "demo": ["demo/local_keycloak.xml"], +} diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..bf62bd7 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100644 index 0000000..eaa8381 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2016 ICTSTUDIO +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import main diff --git a/controllers/__pycache__/__init__.cpython-312.pyc b/controllers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..b432735 Binary files /dev/null and b/controllers/__pycache__/__init__.cpython-312.pyc differ diff --git a/controllers/__pycache__/main.cpython-312.pyc b/controllers/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..ef8b719 Binary files /dev/null and b/controllers/__pycache__/main.cpython-312.pyc differ diff --git a/controllers/main.py b/controllers/main.py new file mode 100644 index 0000000..2104a6c --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,50 @@ +# Copyright 2016 ICTSTUDIO +# Copyright 2021 ACSONE SA/NV +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import base64 +import hashlib +import logging +import secrets + +from werkzeug.urls import url_decode, url_encode + +from odoo.addons.auth_oauth.controllers.main import OAuthLogin + +_logger = logging.getLogger(__name__) + + +class OpenIDLogin(OAuthLogin): + def list_providers(self): + providers = super().list_providers() + for provider in providers: + flow = provider.get("flow") + if flow in ("id_token", "id_token_code"): + params = url_decode(provider["auth_link"].split("?")[-1]) + # nonce + params["nonce"] = secrets.token_urlsafe() + # response_type + if flow == "id_token": + # https://openid.net/specs/openid-connect-core-1_0.html + # #ImplicitAuthRequest + params["response_type"] = "id_token token" + elif flow == "id_token_code": + # https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + params["response_type"] = "code" + # PKCE (https://tools.ietf.org/html/rfc7636) + code_verifier = provider["code_verifier"] + code_challenge = base64.urlsafe_b64encode( + hashlib.sha256(code_verifier.encode("ascii")).digest() + ).rstrip(b"=") + params["code_challenge"] = code_challenge + params["code_challenge_method"] = "S256" + # scope + if provider.get("scope"): + if "openid" not in provider["scope"].split(): + _logger.error("openid connect scope must contain 'openid'") + params["scope"] = provider["scope"] + # auth link that the user will click + provider["auth_link"] = "{}?{}".format( + provider["auth_endpoint"], url_encode(params) + ) + return providers diff --git a/data/auth_oauth_data.xml b/data/auth_oauth_data.xml new file mode 100644 index 0000000..bdeea59 --- /dev/null +++ b/data/auth_oauth_data.xml @@ -0,0 +1,39 @@ + + + + Azure AD Multitenant + id_token_code + False + upn:user_id upn:email + https://login.microsoftonline.com/organizations/oauth2/v2.0/authorize + profile openid + https://login.microsoftonline.com/organizations/oauth2/v2.0/token + https://login.microsoftonline.com/organizations/discovery/v2.0/keys + fa fa-fw fa-windows + Log in with Microsoft + + + Azure AD Single Tenant + id_token_code + False + upn:user_id upn:email + https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/authorize + profile openid + https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token + https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys + fa fa-fw fa-windows + Log in with Microsoft + + diff --git a/demo/local_keycloak.xml b/demo/local_keycloak.xml new file mode 100644 index 0000000..919754d --- /dev/null +++ b/demo/local_keycloak.xml @@ -0,0 +1,20 @@ + + + keycloak:8080 on localhost + id_token_code + auth_oidc-test + preferred_username:user_id + keycloak:8080 on localhost + + openid email + http://localhost:8080/auth/realms/master/protocol/openid-connect/auth + http://localhost:8080/auth/realms/master/protocol/openid-connect/token + http://localhost:8080/auth/realms/master/protocol/openid-connect/certs + + diff --git a/i18n/auth_oidc.pot b/i18n/auth_oidc.pot new file mode 100644 index 0000000..d5aa8dd --- /dev/null +++ b/i18n/auth_oidc.pot @@ -0,0 +1,119 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_oidc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \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_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__flow +msgid "Auth Flow" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__client_secret +msgid "Client Secret" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Code Verifier" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "JWKS URL" +msgstr "" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_multi +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_single +msgid "Log in with Microsoft" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__access_token +msgid "OAuth2" +msgstr "" + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_auth_oauth_provider +msgid "OAuth2 provider" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token_code +msgid "OpenID Connect (authorization code flow)" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token +msgid "OpenID Connect (implicit flow, not recommended)" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Required for OpenID Connect authorization code flow." +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "Required for OpenID Connect." +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_map +msgid "" +"Some Oauth providers don't map keys in their responses exactly as required." +" It is important to ensure user_id and email at least are mapped. For " +"OpenID Connect user_id is the sub key in the standard." +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_map +msgid "Token Map" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Token URL" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Used for PKCE." +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__client_secret +msgid "" +"Used in OpenID Connect authorization code flow for confidential clients." +msgstr "" + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_res_users +msgid "User" +msgstr "" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__validation_endpoint +msgid "UserInfo URL" +msgstr "" + +#. module: auth_oidc +#: model_terms:ir.ui.view,arch_db:auth_oidc.view_oidc_provider_form +msgid "e.g from:to upn:email sub:user_id" +msgstr "" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.local_keycloak +msgid "keycloak:8080 on localhost" +msgstr "" diff --git a/i18n/es.po b/i18n/es.po new file mode 100644 index 0000000..6cda934 --- /dev/null +++ b/i18n/es.po @@ -0,0 +1,128 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_oidc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-10-15 19:36+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__flow +msgid "Auth Flow" +msgstr "Flujo de autenticación" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__client_secret +msgid "Client Secret" +msgstr "Secreto del cliente" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Code Verifier" +msgstr "Verificador del código" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "JWKS URL" +msgstr "URL JWKS" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_multi +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_single +msgid "Log in with Microsoft" +msgstr "Iniciar sesión con Microsoft" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__access_token +msgid "OAuth2" +msgstr "OAuth2" + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_auth_oauth_provider +msgid "OAuth2 provider" +msgstr "Proveedor OAuth2" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token_code +msgid "OpenID Connect (authorization code flow)" +msgstr "OpenID Connect (flujo de código de autorización)" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token +msgid "OpenID Connect (implicit flow, not recommended)" +msgstr "OpenID Connect (flujo implícito, no recomendado)" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Required for OpenID Connect authorization code flow." +msgstr "Necesario para el flujo de código de autorización de OpenID Connect." + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "Required for OpenID Connect." +msgstr "Requerido para OpenID Connect." + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_map +msgid "" +"Some Oauth providers don't map keys in their responses exactly as required. " +"It is important to ensure user_id and email at least are mapped. For OpenID " +"Connect user_id is the sub key in the standard." +msgstr "" +"Algunos proveedores de Oauth no mapean las claves en sus respuestas " +"exactamente como se requiere. Es importante asegurarse de que al menos " +"user_id y email están mapeados. Para OpenID Connect user_id es la subclave " +"en el estándar." + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_map +msgid "Token Map" +msgstr "Mapa de Símbolos" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Token URL" +msgstr "URL de la ficha" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Used for PKCE." +msgstr "Utilizado para PKCE." + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__client_secret +msgid "" +"Used in OpenID Connect authorization code flow for confidential clients." +msgstr "" +"Se utiliza en el flujo de código de autorización de OpenID Connect para " +"clientes confidenciales." + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_res_users +msgid "User" +msgstr "Usuario" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__validation_endpoint +msgid "UserInfo URL" +msgstr "URL de información del usuario" + +#. module: auth_oidc +#: model_terms:ir.ui.view,arch_db:auth_oidc.view_oidc_provider_form +msgid "e.g from:to upn:email sub:user_id" +msgstr "p.ej. from:to upn:email sub:user_id" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.local_keycloak +msgid "keycloak:8080 on localhost" +msgstr "keycloak:8080 en el servidor local" diff --git a/i18n/it.po b/i18n/it.po new file mode 100644 index 0000000..27ba50d --- /dev/null +++ b/i18n/it.po @@ -0,0 +1,127 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_oidc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-05 10:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__flow +msgid "Auth Flow" +msgstr "Flusso atorizzazione" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__client_secret +msgid "Client Secret" +msgstr "Chiave segreta client" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Code Verifier" +msgstr "Verificatore codice" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "JWKS URL" +msgstr "URL JWKS" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_multi +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_single +msgid "Log in with Microsoft" +msgstr "Accedi con Mcrosoft" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__access_token +msgid "OAuth2" +msgstr "OAuth2" + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_auth_oauth_provider +msgid "OAuth2 provider" +msgstr "Provider OAuth2" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token_code +msgid "OpenID Connect (authorization code flow)" +msgstr "OpenID Connect (flusso codice autorizzazione)" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token +msgid "OpenID Connect (implicit flow, not recommended)" +msgstr "OpenID Connect (flusso implicito, non raccomandato)" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Required for OpenID Connect authorization code flow." +msgstr "Richiesto per flusso codice atorizzazione OpenID Connect." + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "Required for OpenID Connect." +msgstr "Richiesto per OpenID Connect." + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_map +msgid "" +"Some Oauth providers don't map keys in their responses exactly as required. " +"It is important to ensure user_id and email at least are mapped. For OpenID " +"Connect user_id is the sub key in the standard." +msgstr "" +"Alcuni Provider Oauth non mappano le chiavi nelle loro risposte esattamente " +"come richiesto. È importante assicurare che almeno user_id ed e-mail siano " +"mappati. Per OpenID Connect user_id è la sotto-chiave nello standard." + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_map +msgid "Token Map" +msgstr "Mappa token" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Token URL" +msgstr "URL token" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Used for PKCE." +msgstr "Utilizzato per PKCE." + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__client_secret +msgid "" +"Used in OpenID Connect authorization code flow for confidential clients." +msgstr "" +"Utilizzato nel flusso codice autorizzazione OpenID Connect per client " +"riservati." + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_res_users +msgid "User" +msgstr "Utente" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__validation_endpoint +msgid "UserInfo URL" +msgstr "URL info utente" + +#. module: auth_oidc +#: model_terms:ir.ui.view,arch_db:auth_oidc.view_oidc_provider_form +msgid "e.g from:to upn:email sub:user_id" +msgstr "es. from:to upn:email sub:user_id" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.local_keycloak +msgid "keycloak:8080 on localhost" +msgstr "keycloak:8080 su localhost" diff --git a/i18n/zh_CN.po b/i18n/zh_CN.po new file mode 100644 index 0000000..4914868 --- /dev/null +++ b/i18n/zh_CN.po @@ -0,0 +1,124 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * auth_oidc +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-07-03 10:47+0000\n" +"Last-Translator: xtanuiha \n" +"Language-Team: none\n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.17\n" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__flow +msgid "Auth Flow" +msgstr "认证流程" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__client_secret +msgid "Client Secret" +msgstr "客户端密钥" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Code Verifier" +msgstr "代码验证器" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "JWKS URL" +msgstr "JWKS 网址" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_multi +#: model:auth.oauth.provider,body:auth_oidc.provider_azuread_single +msgid "Log in with Microsoft" +msgstr "使用 Microsoft 登录" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__access_token +msgid "OAuth2" +msgstr "OAuth2" + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_auth_oauth_provider +msgid "OAuth2 provider" +msgstr "OAuth2 提供者" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token_code +msgid "OpenID Connect (authorization code flow)" +msgstr "OpenID Connect(授权码流程)" + +#. module: auth_oidc +#: model:ir.model.fields.selection,name:auth_oidc.selection__auth_oauth_provider__flow__id_token +msgid "OpenID Connect (implicit flow, not recommended)" +msgstr "OpenID Connect(隐式流程,不推荐)" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Required for OpenID Connect authorization code flow." +msgstr "OpenID Connect 授权码流程所需。" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__jwks_uri +msgid "Required for OpenID Connect." +msgstr "OpenID Connect 所需。" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__token_map +msgid "" +"Some Oauth providers don't map keys in their responses exactly as required. " +"It is important to ensure user_id and email at least are mapped. For OpenID " +"Connect user_id is the sub key in the standard." +msgstr "" +"一些 OAuth 提供者在其响应中并没有完全按照要求映射键。至少需要确保 user_id 和 " +"email 被映射。对于 OpenID Connect,user_id 是标准中的 sub 键。" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_map +msgid "Token Map" +msgstr "令牌映射" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__token_endpoint +msgid "Token URL" +msgstr "令牌网址" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__code_verifier +msgid "Used for PKCE." +msgstr "用于 PKCE。" + +#. module: auth_oidc +#: model:ir.model.fields,help:auth_oidc.field_auth_oauth_provider__client_secret +msgid "" +"Used in OpenID Connect authorization code flow for confidential clients." +msgstr "在 OpenID Connect 授权码流程中用于保密客户端。" + +#. module: auth_oidc +#: model:ir.model,name:auth_oidc.model_res_users +msgid "User" +msgstr "用户" + +#. module: auth_oidc +#: model:ir.model.fields,field_description:auth_oidc.field_auth_oauth_provider__validation_endpoint +msgid "UserInfo URL" +msgstr "用户信息网址" + +#. module: auth_oidc +#: model_terms:ir.ui.view,arch_db:auth_oidc.view_oidc_provider_form +msgid "e.g from:to upn:email sub:user_id" +msgstr "例如 from:to upn:email sub:user_id" + +#. module: auth_oidc +#: model:auth.oauth.provider,body:auth_oidc.local_keycloak +msgid "keycloak:8080 on localhost" +msgstr "localhost 上的 keycloak:8080" diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..ebc8e5b --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2016 ICTSTUDIO +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import auth_oauth_provider +from . import res_users diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..17249cc Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/models/__pycache__/auth_oauth_provider.cpython-312.pyc b/models/__pycache__/auth_oauth_provider.cpython-312.pyc new file mode 100644 index 0000000..528405e Binary files /dev/null and b/models/__pycache__/auth_oauth_provider.cpython-312.pyc differ diff --git a/models/__pycache__/res_users.cpython-312.pyc b/models/__pycache__/res_users.cpython-312.pyc new file mode 100644 index 0000000..239031b Binary files /dev/null and b/models/__pycache__/res_users.cpython-312.pyc differ diff --git a/models/auth_oauth_provider.py b/models/auth_oauth_provider.py new file mode 100644 index 0000000..ac498a7 --- /dev/null +++ b/models/auth_oauth_provider.py @@ -0,0 +1,106 @@ +# Copyright 2016 ICTSTUDIO +# Copyright 2021 ACSONE SA/NV +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging +import secrets + +import requests + +from odoo import fields, models, tools + +try: + from jose import jwt + from jose.exceptions import JWSError, JWTError +except ImportError: + logging.getLogger(__name__).debug("jose library not installed") + + +class AuthOauthProvider(models.Model): + _inherit = "auth.oauth.provider" + + flow = fields.Selection( + [ + ("access_token", "OAuth2"), + ("id_token_code", "OpenID Connect (authorization code flow)"), + ("id_token", "OpenID Connect (implicit flow, not recommended)"), + ], + string="Auth Flow", + required=True, + default="access_token", + ) + token_map = fields.Char( + help="Some Oauth providers don't map keys in their responses " + "exactly as required. It is important to ensure user_id and " + "email at least are mapped. For OpenID Connect user_id is " + "the sub key in the standard." + ) + client_secret = fields.Char( + help="Used in OpenID Connect authorization code flow for confidential clients.", + ) + code_verifier = fields.Char( + default=lambda self: secrets.token_urlsafe(32), help="Used for PKCE." + ) + validation_endpoint = fields.Char(required=False) + token_endpoint = fields.Char( + string="Token URL", help="Required for OpenID Connect authorization code flow." + ) + jwks_uri = fields.Char(string="JWKS URL", help="Required for OpenID Connect.") + + @tools.ormcache("self.jwks_uri", "kid") + def _get_keys(self, kid): + r = requests.get(self.jwks_uri, timeout=10) + r.raise_for_status() + response = r.json() + # the keys returned here should follow + # JWS Notes on Key Selection + # https://datatracker.ietf.org/doc/html/draft-ietf-jose-json-web-signature#appendix-D + return [ + key + for key in response["keys"] + if kid is None or key.get("kid", None) == kid + ] + + def _map_token_values(self, res): + if self.token_map: + for pair in self.token_map.split(" "): + from_key, to_key = (k.strip() for k in pair.split(":", 1)) + if to_key not in res: + res[to_key] = res.get(from_key, "") + return res + + def _parse_id_token(self, id_token, access_token): + self.ensure_one() + res = {} + header = jwt.get_unverified_header(id_token) + res.update(self._decode_id_token(access_token, id_token, header.get("kid"))) + res.update(self._map_token_values(res)) + return res + + def _decode_id_token(self, access_token, id_token, kid): + keys = self._get_keys(kid) + if len(keys) > 1 and kid is None: + # https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.10.1 + # If there are multiple keys in the referenced JWK Set document, a kid + # value MUST be provided in the JOSE Header. + raise JWTError( + "OpenID Connect requires kid to be set if there is more" + " than one key in the JWKS" + ) + error = None + # we accept multiple keys with the same kid in case a key gets rotated. + for key in keys: + try: + values = jwt.decode( + id_token, + key, + algorithms=["RS256"], + audience=self.client_id, + access_token=access_token, + ) + return values + except (JWTError, JWSError) as e: + error = e + if error: + raise error + return {} diff --git a/models/res_users.py b/models/res_users.py new file mode 100644 index 0000000..1684480 --- /dev/null +++ b/models/res_users.py @@ -0,0 +1,82 @@ +# Copyright 2016 ICTSTUDIO +# Copyright 2021 ACSONE SA/NV +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import logging + +import requests + +from odoo import api, models +from odoo.exceptions import AccessDenied +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class ResUsers(models.Model): + _inherit = "res.users" + + def _auth_oauth_get_tokens_implicit_flow(self, oauth_provider, params): + # https://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthResponse + return params.get("access_token"), params.get("id_token") + + def _auth_oauth_get_tokens_auth_code_flow(self, oauth_provider, params): + # https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse + code = params.get("code") + # https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest + auth = None + if oauth_provider.client_secret: + auth = (oauth_provider.client_id, oauth_provider.client_secret) + response = requests.post( + oauth_provider.token_endpoint, + data=dict( + client_id=oauth_provider.client_id, + grant_type="authorization_code", + code=code, + code_verifier=oauth_provider.code_verifier, # PKCE + redirect_uri=request.httprequest.url_root + "auth_oauth/signin", + ), + auth=auth, + timeout=10, + ) + response.raise_for_status() + response_json = response.json() + # https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse + return response_json.get("access_token"), response_json.get("id_token") + + @api.model + def auth_oauth(self, provider, params): + oauth_provider = self.env["auth.oauth.provider"].browse(provider) + if oauth_provider.flow == "id_token": + access_token, id_token = self._auth_oauth_get_tokens_implicit_flow( + oauth_provider, params + ) + elif oauth_provider.flow == "id_token_code": + access_token, id_token = self._auth_oauth_get_tokens_auth_code_flow( + oauth_provider, params + ) + else: + return super().auth_oauth(provider, params) + if not access_token: + _logger.error("No access_token in response.") + raise AccessDenied() + if not id_token: + _logger.error("No id_token in response.") + raise AccessDenied() + validation = oauth_provider._parse_id_token(id_token, access_token) + # required check + if "sub" in validation and "user_id" not in validation: + # set user_id for auth_oauth, user_id is not an OpenID Connect standard + # claim: + # https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + validation["user_id"] = validation["sub"] + elif not validation.get("user_id"): + _logger.error("user_id claim not found in id_token (after mapping).") + raise AccessDenied() + # retrieve and sign in user + params["access_token"] = access_token + login = self._auth_oauth_signin(provider, validation, params) + if not login: + raise AccessDenied() + # return user credentials + return (self.env.cr.dbname, login, access_token) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/readme/CONFIGURE.md b/readme/CONFIGURE.md new file mode 100644 index 0000000..275e4c0 --- /dev/null +++ b/readme/CONFIGURE.md @@ -0,0 +1,72 @@ +## Setup for Microsoft Azure + +Example configuration with OpenID Connect authorization code flow. + +1. configure a new web application in Azure with OpenID and code flow (see +the [provider +documentation](https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-openid-provider))) + +2. in this application the redirect url must be be "\/auth_oauth/signin" and of course this URL should be reachable +from Azure + +3. create a new authentication provider in Odoo with the following +parameters (see the [portal +documentation](https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/configure-openid-settings) +for more information): + +![image](../static/description/oauth-microsoft_azure-api_permissions.png) + +![image](../static/description/oauth-microsoft_azure-optional_claims.png) + +Single tenant provider limits the access to user of your tenant, while +Multitenants allow access for all AzureAD users, so user of foreign +companies can use their AzureAD login without an guest account. + +- Provider Name: Azure AD Single Tenant +- Client ID: Application (client) id +- Client Secret: Client secret +- Allowed: yes + +or + +- Provider Name: Azure AD Multitenant +- Client ID: Application (client) id +- Client Secret: Client secret +- Allowed: yes +- replace {tenant_id} in urls with your Azure tenant id + +![image](../static/description/odoo-azure_ad_multitenant.png) + +## Setup for Keycloak + +Example configuration with OpenID Connect authorization code flow. + +In Keycloak: + +1. configure a new Client +2. make sure Authorization Code Flow is +Enabled. +3. configure the client Access Type as "confidential" and take +note of the client secret in the Credentials tab +4. configure the +redirect url to be "\/auth_oauth/signin" + +In Odoo, create a new Oauth Provider with the following parameters: + +- Provider name: Keycloak (or any name you like that identify your + keycloak provider) +- Auth Flow: OpenID Connect (authorization code flow) +- Client ID: the same Client ID you entered when configuring the client + in Keycloak +- Client Secret: found in keycloak on the client Credentials tab +- Allowed: yes +- Body: the link text to appear on the login page, such as Login with + Keycloak +- Scope: openid email +- Authentication URL: The "authorization_endpoint" URL found in the + OpenID Endpoint Configuration of your Keycloak realm +- Token URL: The "token_endpoint" URL found in the OpenID Endpoint + Configuration of your Keycloak realm +- JWKS URL: The "jwks_uri" URL found in the OpenID Endpoint + Configuration of your Keycloak realm diff --git a/readme/CONTRIBUTORS.md b/readme/CONTRIBUTORS.md new file mode 100644 index 0000000..8bdbc1d --- /dev/null +++ b/readme/CONTRIBUTORS.md @@ -0,0 +1,4 @@ +- Alexandre Fayolle \<\> +- Stéphane Bidoul \<\> +- David Jaen \<\> +- Andreas Perhab \<\> diff --git a/readme/DESCRIPTION.md b/readme/DESCRIPTION.md new file mode 100644 index 0000000..3677c8b --- /dev/null +++ b/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module allows users to login through an OpenID Connect provider +using the authorization code flow or implicit flow. + +Note the implicit flow is not recommended because it exposes access +tokens to the browser and in http logs. diff --git a/readme/HISTORY.md b/readme/HISTORY.md new file mode 100644 index 0000000..48ad92f --- /dev/null +++ b/readme/HISTORY.md @@ -0,0 +1,39 @@ +## 18.0.1.0.0 2024-10-09 + +- Odoo 18 migration + +## 17.0.1.0.0 2024-03-20 + +- Odoo 17 migration + +## 16.0.1.1.0 2024-02-28 + +- Forward port OpenID Connect fixes from 15.0 to 16.0 + +## 16.0.1.0.2 2023-11-16 + +- Readme link updates + +## 16.0.1.0.1 2023-10-09 + +- Add AzureAD code flow provider + +## 16.0.1.0.0 2023-01-27 + +- Odoo 16 migration + +## 15.0.1.0.0 2023-01-06 + +- Odoo 15 migration + +## 14.0.1.0.0 2021-12-10 + +- Odoo 14 migration + +## 13.0.1.0.0 2020-04-10 + +- Odoo 13 migration, add authorization code flow. + +## 10.0.1.0.0 2018-10-05 + +- Initial implementation diff --git a/readme/INSTALL.md b/readme/INSTALL.md new file mode 100644 index 0000000..37af7b9 --- /dev/null +++ b/readme/INSTALL.md @@ -0,0 +1,3 @@ +This module depends on the +[python-jose](https://pypi.org/project/python-jose/) library, not to be +confused with `jose` which is also available on PyPI. diff --git a/readme/ROADMAP.md b/readme/ROADMAP.md new file mode 100644 index 0000000..712da2f --- /dev/null +++ b/readme/ROADMAP.md @@ -0,0 +1,4 @@ +- When going to the login screen, check for a existing token and do a + direct login without the clicking on the SSO link +- When doing a logout an extra option to also logout at the SSO + provider. diff --git a/readme/USAGE.md b/readme/USAGE.md new file mode 100644 index 0000000..0fa7425 --- /dev/null +++ b/readme/USAGE.md @@ -0,0 +1 @@ +On the login page, click on the authentication provider you configured. diff --git a/static/description/icon.png b/static/description/icon.png new file mode 100644 index 0000000..06a05eb Binary files /dev/null and b/static/description/icon.png differ diff --git a/static/description/index.html b/static/description/index.html new file mode 100644 index 0000000..8af7bef --- /dev/null +++ b/static/description/index.html @@ -0,0 +1,607 @@ + + + + + +Authentication OpenID Connect + + + +
+

Authentication OpenID Connect

+ + +

Beta License: AGPL-3 OCA/server-auth Translate me on Weblate Try me on Runboat

+

This module allows users to login through an OpenID Connect provider +using the authorization code flow or implicit flow.

+

Note the implicit flow is not recommended because it exposes access +tokens to the browser and in http logs.

+

Table of contents

+ +
+

Installation

+

This module depends on the +python-jose library, not to +be confused with jose which is also available on PyPI.

+
+
+

Configuration

+
+

Setup for Microsoft Azure

+

Example configuration with OpenID Connect authorization code flow.

+
    +
  1. configure a new web application in Azure with OpenID and code flow +(see the provider +documentation))
  2. +
  3. in this application the redirect url must be be “<url of your +server>/auth_oauth/signin” and of course this URL should be reachable +from Azure
  4. +
  5. create a new authentication provider in Odoo with the following +parameters (see the portal +documentation +for more information):
  6. +
+

image

+

image1

+

Single tenant provider limits the access to user of your tenant, while +Multitenants allow access for all AzureAD users, so user of foreign +companies can use their AzureAD login without an guest account.

+
    +
  • Provider Name: Azure AD Single Tenant
  • +
  • Client ID: Application (client) id
  • +
  • Client Secret: Client secret
  • +
  • Allowed: yes
  • +
+

or

+
    +
  • Provider Name: Azure AD Multitenant
  • +
  • Client ID: Application (client) id
  • +
  • Client Secret: Client secret
  • +
  • Allowed: yes
  • +
  • replace {tenant_id} in urls with your Azure tenant id
  • +
+

image2

+
+
+

Setup for Keycloak

+

Example configuration with OpenID Connect authorization code flow.

+

In Keycloak:

+
    +
  1. configure a new Client
  2. +
  3. make sure Authorization Code Flow is Enabled.
  4. +
  5. configure the client Access Type as “confidential” and take note of +the client secret in the Credentials tab
  6. +
  7. configure the redirect url to be “<url of your +server>/auth_oauth/signin”
  8. +
+

In Odoo, create a new Oauth Provider with the following parameters:

+
    +
  • Provider name: Keycloak (or any name you like that identify your +keycloak provider)
  • +
  • Auth Flow: OpenID Connect (authorization code flow)
  • +
  • Client ID: the same Client ID you entered when configuring the client +in Keycloak
  • +
  • Client Secret: found in keycloak on the client Credentials tab
  • +
  • Allowed: yes
  • +
  • Body: the link text to appear on the login page, such as Login with +Keycloak
  • +
  • Scope: openid email
  • +
  • Authentication URL: The “authorization_endpoint” URL found in the +OpenID Endpoint Configuration of your Keycloak realm
  • +
  • Token URL: The “token_endpoint” URL found in the OpenID Endpoint +Configuration of your Keycloak realm
  • +
  • JWKS URL: The “jwks_uri” URL found in the OpenID Endpoint +Configuration of your Keycloak realm
  • +
+
+
+
+

Usage

+

On the login page, click on the authentication provider you configured.

+
+
+

Known issues / Roadmap

+
    +
  • When going to the login screen, check for a existing token and do a +direct login without the clicking on the SSO link
  • +
  • When doing a logout an extra option to also logout at the SSO +provider.
  • +
+
+
+

Changelog

+
+

18.0.1.0.0 2024-10-09

+
    +
  • Odoo 18 migration
  • +
+
+
+

17.0.1.0.0 2024-03-20

+
    +
  • Odoo 17 migration
  • +
+
+
+

16.0.1.1.0 2024-02-28

+
    +
  • Forward port OpenID Connect fixes from 15.0 to 16.0
  • +
+
+
+

16.0.1.0.2 2023-11-16

+
    +
  • Readme link updates
  • +
+
+
+

16.0.1.0.1 2023-10-09

+
    +
  • Add AzureAD code flow provider
  • +
+
+
+

16.0.1.0.0 2023-01-27

+
    +
  • Odoo 16 migration
  • +
+
+
+

15.0.1.0.0 2023-01-06

+
    +
  • Odoo 15 migration
  • +
+
+
+

14.0.1.0.0 2021-12-10

+
    +
  • Odoo 14 migration
  • +
+
+
+

13.0.1.0.0 2020-04-10

+
    +
  • Odoo 13 migration, add authorization code flow.
  • +
+
+
+

10.0.1.0.0 2018-10-05

+
    +
  • Initial implementation
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ICTSTUDIO
  • +
  • André Schenkels
  • +
  • ACSONE SA/NV
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

sbidoul

+

This module is part of the OCA/server-auth project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/static/description/oauth-microsoft_azure-api_permissions.png b/static/description/oauth-microsoft_azure-api_permissions.png new file mode 100644 index 0000000..7f28000 Binary files /dev/null and b/static/description/oauth-microsoft_azure-api_permissions.png differ diff --git a/static/description/oauth-microsoft_azure-optional_claims.png b/static/description/oauth-microsoft_azure-optional_claims.png new file mode 100644 index 0000000..1aa7206 Binary files /dev/null and b/static/description/oauth-microsoft_azure-optional_claims.png differ diff --git a/static/description/odoo-azure_ad_multitenant.png b/static/description/odoo-azure_ad_multitenant.png new file mode 100644 index 0000000..31033f3 Binary files /dev/null and b/static/description/odoo-azure_ad_multitenant.png differ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e603d99 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +from . import test_auth_oidc_auth_code diff --git a/tests/keycloak/keycloak-config.json b/tests/keycloak/keycloak-config.json new file mode 100644 index 0000000..5a0456b --- /dev/null +++ b/tests/keycloak/keycloak-config.json @@ -0,0 +1,1997 @@ +{ + "id": "master", + "realm": "master", + "displayName": "Keycloak", + "displayNameHtml": "
Keycloak
", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 60, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "39f27ebe-139e-435b-840a-beb824d5d355", + "name": "admin", + "description": "${role_admin}", + "composite": true, + "composites": { + "realm": ["create-realm"], + "client": { + "master-realm": [ + "create-client", + "view-realm", + "view-events", + "manage-clients", + "query-clients", + "view-identity-providers", + "impersonation", + "manage-events", + "query-realms", + "query-groups", + "manage-authorization", + "query-users", + "view-authorization", + "manage-identity-providers", + "manage-users", + "view-clients", + "view-users", + "manage-realm" + ] + } + }, + "clientRole": false, + "containerId": "master", + "attributes": {} + }, + { + "id": "3fd38fac-f708-4783-b8e9-4e47963fc4bf", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "master", + "attributes": {} + }, + { + "id": "4ac4a81b-0a30-41db-94ce-dbd621c331d2", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "master", + "attributes": {} + }, + { + "id": "20b16986-2361-454c-af0b-81f403152ef8", + "name": "create-realm", + "description": "${role_create-realm}", + "composite": false, + "clientRole": false, + "containerId": "master", + "attributes": {} + } + ], + "client": { + "auth_oidc-test": [], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "5fa108a0-2e5e-4e2e-8ee3-1317592517f8", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "d1dd5ade-20bf-4d53-a371-a33f10bc1087", + "attributes": {} + } + ], + "master-realm": [ + { + "id": "0d062a1c-5165-4ea5-b550-55d02ca86226", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "f5795bcb-ab2d-4a74-b954-51f335c21198", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "1c0e3231-db03-4b03-961f-32308318f4f1", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "ed9949d1-b11d-4742-b259-ee260f62f111", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "b29b494a-9cd4-4410-8d16-207a3bb2e528", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "8afdd284-070b-4d6f-9d21-d9917d8827af", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "b5807416-f8f3-41ae-a29e-298ec3aae028", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "6400b20c-a72b-4228-87d1-01b0d1315026", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "6c3b14c3-0797-4e39-b5fa-b07d0e073e0e", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "3fbf2279-9661-4cfc-b381-c7e4a8c459dc", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "1b9e5572-34d5-4284-897c-0471544cf813", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "a226e0fa-aa45-490d-9d64-78e88c3152cb", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "d78736d4-250c-4012-a8ad-55b5c718a57a", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "68e9559f-d467-46c3-ae48-d67c74582ca8", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "908beec6-d8d1-441a-a5ad-f45d39df6b43", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "5c5d56a2-e2ea-4b4f-9bb1-f40e18082932", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "master-realm": ["query-clients"] + } + }, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "561ec0f4-bd97-4c41-a825-918002afb307", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "master-realm": ["query-groups", "query-users"] + } + }, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + }, + { + "id": "8ae350ac-19cd-431b-80fb-aee88316219e", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "attributes": {} + } + ], + "account": [ + { + "id": "0a6ac4dd-afdc-4b7b-b16a-ef2ca3b8e396", + "name": "delete-account", + "description": "${role_delete-account}", + "composite": false, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + }, + { + "id": "1a30b2d0-9d49-4a09-9769-ea7f3142e715", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": ["view-consent"] + } + }, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + }, + { + "id": "e065f4ba-d97c-4219-b56e-edbe945e14bf", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + }, + { + "id": "6e5b9a43-0f82-4669-a821-a1d449e4a2be", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + }, + { + "id": "c386c8c5-bdee-4d52-b124-94379799d5d9", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + }, + { + "id": "bcff49f4-7f83-4ec6-9a51-271fc9cbb302", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + }, + { + "id": "4e36a4bf-80ab-404b-854e-7d593e9248ca", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": ["manage-account-links"] + } + }, + "clientRole": true, + "containerId": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRoles": ["offline_access", "uma_authorization"], + "requiredCredentials": ["password"], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": ["FreeOTP", "Google Authenticator"], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "users": [ + { + "id": "ad01a4d9-c919-4bc6-8b48-b3e3bcbb4149", + "createdTimestamp": 1618140941731, + "username": "admin", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "596b17bb-199c-4a23-9c48-4620c0ecfd7a", + "type": "password", + "createdDate": 1618140941876, + "secretData": "{\"value\":\"PXx46hQETQuXQRUl9FvzEJdZtoL57qsad1dFQyOLzj/pNEmwldN54oxQh5p+QB0rNNJPI9ZiaAfZS90ZzJa6pQ==\",\"salt\":\"kiFQwyPm53MgwAByqTw5qQ==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["offline_access", "admin", "uma_authorization"], + "clientRoles": { + "account": ["view-profile", "manage-account"] + }, + "notBefore": 0, + "groups": [] + }, + { + "id": "3752dfb8-d3b5-4597-b83c-fed005d2671c", + "createdTimestamp": 1618141153912, + "username": "demo", + "enabled": true, + "totp": false, + "emailVerified": false, + "credentials": [ + { + "id": "4e5b5a38-3fcb-4703-8b9c-075164dde145", + "type": "password", + "createdDate": 1618141311783, + "secretData": "{\"value\":\"upShAwzTaS89elSkEgK0Phs+XUP3Ya1pOUYtE8k4JmZEJnXWjdOy9brn4cpLKwjF6pZ3glxkJgjdLmDeWm9WwQ==\",\"salt\":\"RnaXCbRf4bw1lZmQX43cMg==\",\"additionalParameters\":{}}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["offline_access", "uma_authorization"], + "clientRoles": { + "account": ["view-profile", "manage-account"] + }, + "notBefore": 0, + "groups": [] + } + ], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": ["offline_access"] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": ["manage-account"] + } + ] + }, + "clients": [ + { + "id": "45ef915f-ca72-4bcc-a79b-d2b83ca4be2f", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "055c06d1-ffcc-4762-b8eb-e9814a6995df", + "defaultRoles": ["view-profile", "manage-account"], + "redirectUris": ["/realms/master/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "4308a071-7dbf-4d08-a987-b7dc2f42b86e", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/master/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "ec31839f-7ffb-400d-9373-26be6706e619", + "redirectUris": ["/realms/master/account/*"], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "e36bba2d-7a07-4c83-a40e-14b4a8316ae9", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "0915d9fc-102f-4033-b37e-832b89fee932", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "175303e4-f2d4-4ae9-8fb0-27a1337d3208", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "8bf21eb5-63da-4da5-8c12-7a5bafda1bf5", + "clientId": "auth_oidc-test", + "rootUrl": "http://localhost:8069", + "adminUrl": "http://localhost:8069", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "20c0ad33-0200-43cd-9bd1-5dd1b22918e3", + "redirectUris": ["http://localhost:8069/*"], + "webOrigins": ["http://localhost:8069"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "backchannel.logout.revoke.offline.tokens": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "backchannel.logout.session.required": "true", + "client_credentials.use_refresh_token": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "d1dd5ade-20bf-4d53-a371-a33f10bc1087", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "52aab659-5f2d-445a-a93e-6ab04de9db42", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "4e03d0f9-4c56-42c8-958c-337f6eede3ac", + "clientId": "master-realm", + "name": "master Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "15ca5d76-d964-4761-85d1-8343748481ab", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "983b74a1-e7a0-4bc4-8481-0eb8ca4f12e0", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/master/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "121beec3-2388-475b-b989-1ff85d25b4fd", + "redirectUris": ["/admin/master/console/*"], + "webOrigins": ["+"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "21a0f25a-a2b7-415e-95a7-23886f00c83b", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "role_list", "roles", "profile", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + } + ], + "clientScopes": [ + { + "id": "07bff9f2-498f-4f07-9fb9-019152141a0f", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "46333a31-1e88-4191-8d25-2f4d975af4db", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "8c30d3a8-af56-407a-a622-df16e7c2b04b", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "db1117e1-6174-4934-99d0-ff51f319c6f5", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "c1a7d21e-7f45-4559-b314-913cbb560967", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "3d9f47ed-d2f0-474c-baa4-75e0e6619385", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "e7c49c89-b513-4512-9410-bc0f39d4b4e5", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "7b65fe0b-2974-445f-a7cd-ccec5141f560", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "a9ed1a18-0e05-4eeb-a247-c84296cfa653", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "52f62f87-f914-4771-b79c-46a387797be7", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d8c485dc-7b8a-4469-8868-5bb73a6abafa", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + }, + { + "id": "b1b21cd7-06f7-4469-b11e-22bf9de1a3ce", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "311bf186-6cb2-4bdd-9da5-8ac30ff8b296", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "eda5d01f-6841-441e-b7d0-8fa48365a501", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "e131db35-e08b-4805-b11b-59a90650ee2a", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "6e311016-71e1-450a-8011-6f6ce9f7e365", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "e0fc1d75-2b19-4fc8-8ea3-778c96b47321", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "a94d4acf-0a46-44b0-a739-ed329dfa9f41", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "57d4c1ad-4507-4545-8efc-5f83c0dc0be6", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "0b42528f-d116-4217-9fbe-fe469c80914d", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + }, + { + "id": "f698a281-a28c-4b3e-ad73-524c556d1cc5", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "50b2c4f9-b4a3-470a-a8e4-af7384bd9536", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "12a21276-1f49-4b34-8bf7-df4dd7929ebf", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "String" + } + }, + { + "id": "4abce20a-c47d-430a-85c0-f65cb9ae7aa0", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "fa7c99b6-9cdf-4f8f-96af-45df5f2497e2", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "983d7499-e23c-4971-9501-d404b405b484", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "f42b6bcb-4bac-44a8-8073-c3194b56029c", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "6acc2950-2bbe-49d8-b266-42e3c518f46f", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "8b0f3195-f1c2-4188-8045-5d48ba0aeb30", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "437e71e0-7728-4763-82af-29fd14b1fa12", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "d255020d-b385-417d-bf27-b3a8c5911579", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "5adb2a9c-5dbb-430c-89c0-9b464677245f", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "667e731f-4375-489f-bf03-566a7292719b", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "359211aa-1a6d-4441-a9bb-610acc6350db", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "6646f9a5-0a1e-4bcc-b1d1-35b035b5aa55", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": ["jboss-logging"], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "070f65ee-8d8f-4fff-ae32-a27016e7bf5c", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "81bc44f5-9bbd-4325-9f63-fc332e30e0e7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper", + "saml-user-attribute-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "f7d673f4-10ef-486d-a168-925b139abbc5", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "c103a051-9371-4021-8a34-8196a78c3638", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": ["true"] + } + }, + { + "id": "a71eaf13-3e55-4aa6-9cf8-c1b74198d63a", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": ["true"], + "client-uris-must-match": ["true"] + } + }, + { + "id": "fb7bdacb-46bc-4266-b217-a1705e87f957", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-attribute-mapper", + "saml-user-property-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "saml-role-list-mapper" + ] + } + }, + { + "id": "ec4f12ef-2fef-42b2-9cb8-9f251d8c3344", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "99778abc-51f8-4480-a778-59ef73a60f56", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": ["200"] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "052d41cb-6938-40f0-8872-8ab171ec27e9", + "name": "fallback-HS256", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": ["489fe44c-d70f-42f7-9a19-e3dc2a0df076"], + "secret": [ + "6dqvUhGU5rhuMOKNuOI4U7nPTcA9jeJJLpmoewnkw_PdFDSjy73iQkPt5hw_8qU34IIFGOM-LkJJ8VWihvwEwQ" + ], + "priority": ["-100"], + "algorithm": ["HS256"] + } + }, + { + "id": "53901ca8-2f9d-4f2e-804f-756204b9c1c5", + "name": "fallback-RS256", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEpAIBAAKCAQEAgYNR3Pgh/f1+DUcMBc9T6uT1MwC4oTthGbtJhmqQiawSWzUO8icSM4hFjiN2zqsKx7ofWmP3+ZRTq6fSEref+0tRRWafTq6LtDySa4DilqnQ/WBznnXML9hmsPBW3gNiZAKYbwvb/YE36L+a4nWcEc13jcXgMqLXUD7K/3YIYNT/S7xGgNKYfBmbTfS0A08ZITtyakaGYwLK6zwLYVAeUj1hZjVcz1926Zhhu4YznD6qMgCmBwlSD9lc6v0/RUNjo1NKSU0LXAeUEk1ynxFKJ+cUikHuvsIQvuXY5Sj+Y2tcWFpU51J01com69kdyYeelQv1n41yOB/U4bGmbhUctwIDAQABAoIBAH+RgwwdmRXeH9AiQBRk8Gq5lU/kkPe3TmCTGsv8oVwKEpamP4+Drqj1vFVSV08gKOEsUn+tYm8CjBvTlNd86WcT+/xZJefRg6hH1Y1wiUAQCtvYqmnV7Abgp933Dglm2f5alB0lWE5ufkySlpQjdlQOx4js9HXL8juHblqMv5noJNaDQSDh4UxtET+fVT8pvCL/MImG4C6BtULLDXLdH4pIvn0OIS788Xpc87uc3dSIfVxL1Oa0U1Qrxma5P8p9imremKLdA4iOzopyVsLo0uP2PrSLWD04I9kSwO0MHNDzbkMJiWXX5a7afzRY4g+zQL2STYsbD4B9KQnIpsQNrVkCgYEA4LbUejeexUBsjs3Whkn1BlUvxvDf2Vtudws1XSNmd5lneitfseDXCcH6p1TLHn0xoOmJDFGjLPEYFhqL0I5IyZP6zfiCJL88zFWVXlY8NsAoQeqvgnu5wHpIXEXCaJAAksWy/dmUZwTUyfIIxnLUQMPpJ1stu35e8DyNO4VadPMCgYEAk4teFNWkFBZmjSBUyo8Tw4TmJuCIRVH4FSaspUeVNhToKI3e5R/duz8rvqBj4tul5lyWq9FmcDaawE94jIa17XQnj/O767G72lHRuIlI+qftIca3r4/kDvy730yAOWl/1Su4SrX3t7WSBHIG2j7HMYIsj5xgBUvnbRQUtxByui0CgYEAsLe3YyHoj3D5rlg708HHmqJVf1sgfxvDRIUhA0z6oSWX1eDUUdvi4H6XMw6g6ipEZCokJ/bvn0E+0usvduTeYwAn5eD/4AwwsPTBEb45fkkhn60DN1c7nh3MWBxYJcjRWpt1BuMcLOQEv4fC1OWq+//VlKjEz0UzPjQwUVWu7HcCgYAo6uqhfootY/T2yHObZUiG3ZFyUKyaBNx3CS2x/IMd53hm3slk44x7hE5eZF6vKFj+5MiIR99P2WTbVm7JEgbcHm1mV6LS/4xoRG6T7cbGdNGnn1OLpa0Klv6HM9EPmvlvpdtLJOHZGcqv3uuVlPlq+n3fKe/bKCy7LGl+R1p51QKBgQCEmPS9A9y6YF7zRo8u7vUmJzGktdrSw65zYZVMzXY9A7uKU/OfpZ+papKDr1D9ApFgi5Ip1imirR7K9m4GImowZOTe/E6dT6nmrUtWUkaS4ghhwZ9Gh6kAOWoBYRB/Z4XIzJoSiet+PJ3p8SLhM7nETj7IDaeQgNudSK8/ohNPWQ==" + ], + "certificate": [ + "MIICmzCCAYMCBgF4wLeSOTANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjEwNDExMTEzNDE5WhcNMzEwNDExMTEzNTU5WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCBg1Hc+CH9/X4NRwwFz1Pq5PUzALihO2EZu0mGapCJrBJbNQ7yJxIziEWOI3bOqwrHuh9aY/f5lFOrp9ISt5/7S1FFZp9Orou0PJJrgOKWqdD9YHOedcwv2Gaw8FbeA2JkAphvC9v9gTfov5ridZwRzXeNxeAyotdQPsr/dghg1P9LvEaA0ph8GZtN9LQDTxkhO3JqRoZjAsrrPAthUB5SPWFmNVzPX3bpmGG7hjOcPqoyAKYHCVIP2Vzq/T9FQ2OjU0pJTQtcB5QSTXKfEUon5xSKQe6+whC+5djlKP5ja1xYWlTnUnTVyibr2R3Jh56VC/WfjXI4H9ThsaZuFRy3AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAHEq+7bncqOh0RJJj+6fSHsIlkRGOeX6djVIKi1/eAJCD61Et3MHKh4kbu4U6phNlnhW5IFYinchGXe1uoG18fWkUS6QJoxHIDLR+tub7NSMraYxK85VgyLHCHaaGX7Bz+sIM628th4LlQd/M2zL45rqlMvB1XLxsMpi9Pb0Zc7qWwrvE5Jfi99UDAi6ZV3OojR6YC79HVHyOVmBIdLrVtn5mQYKJ5tF5F8xSs4ng96IO8Sn8pbUuYG8SlEz6KMmGH1sczlPE/3kAdm9IF+fXpYywuhsRNJyDBVDGpcqHTW+UW+V5TWa/ucZ6cpr1dQP5/FpcHylSWoXJpCk01PXl/M=" + ], + "priority": ["-100"], + "algorithm": ["RS256"] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "815c3100-241b-4298-8039-54253c2c7e70", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "e6a85d70-dc13-4705-9ba3-a980d548a430", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "96b14b20-7305-4018-8e75-86b572e3ace0", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "a51abcaa-7495-4526-997f-55cd82f152e2", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "54fa7607-f4e5-4fe7-a8c3-4fad6ee376b8", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "31de3d9e-2f81-43cc-a4ad-1112cd4e9d71", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f499a904-41cc-41c1-bb43-688672d1533b", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "7cee4451-5b17-4742-8736-8fce85639409", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "dafe7404-bce7-4ab6-9e35-fd1aecb93545", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d34ac568-3793-4864-8df8-733fa2cd3554", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "15a43900-948a-487d-a97a-444502dce766", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "1865917e-f56d-453c-bb66-578e1955d199", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "3e151380-0869-4bdb-b9d1-65a7bcfcb6ab", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "79369bf2-9434-4b93-94d9-74a08a361701", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "322d8eb9-a9cc-422a-a6a2-065b3b863967", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "0cf68f4f-332e-4f7d-a7a9-907189914191", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d412906e-1aad-4707-ac40-95406aeed8d0", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "d11ddacf-4fe8-4614-a9f1-fe5dcf621330", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "48093169-4a24-499b-aad7-b87dcd32269d", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f0a1d44c-2bab-408c-91b4-bb730bd65bc4", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "173229da-6a52-4d54-8e88-cba503234cb4", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "a72c2a36-a48a-440d-b52f-831c385287fc", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": {}, + "keycloakVersion": "12.0.4", + "userManagedAccessAllowed": false +} diff --git a/tests/keycloak/keycloak.sh b/tests/keycloak/keycloak.sh new file mode 100644 index 0000000..7c81d56 --- /dev/null +++ b/tests/keycloak/keycloak.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -x +$(which docker || which podman) run --rm \ + -v $(dirname $0)/keycloak-config.json:/tmp/keycloak-config.json \ + -p 8080:8080 \ + quay.io/keycloak/keycloak:12.0.4 \ + -Dkeycloak.migration.action=import \ + -Dkeycloak.migration.provider=singleFile \ + -Dkeycloak.migration.file=/tmp/keycloak-config.json \ + -Dkeycloak.migration.strategy=OVERWRITE_EXISTING diff --git a/tests/test_auth_oidc_auth_code.py b/tests/test_auth_oidc_auth_code.py new file mode 100644 index 0000000..a833407 --- /dev/null +++ b/tests/test_auth_oidc_auth_code.py @@ -0,0 +1,321 @@ +# Copyright 2021 ACSONE SA/NV +# License: AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import contextlib +import json +import logging +from urllib.parse import parse_qs, urlparse + +import responses +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from jose import jwt +from jose.exceptions import JWTError +from jose.utils import long_to_base64 + +import odoo +from odoo.exceptions import AccessDenied +from odoo.tests import common + +from odoo.addons.website.tools import MockRequest as _MockRequest + +from ..controllers.main import OpenIDLogin + +BASE_URL = f"http://localhost:{odoo.tools.config['http_port']}" + + +@contextlib.contextmanager +def MockRequest(env): + with _MockRequest(env) as request: + request.httprequest.url_root = BASE_URL + "/" + request.params = {} + yield request + + +class TestAuthOIDCAuthorizationCodeFlow(common.HttpCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + ( + cls.rsa_key_pem, + cls.rsa_key_public_pem, + cls.rsa_key_public_jwk, + ) = cls._generate_key() + _, cls.second_key_public_pem, _ = cls._generate_key() + + @staticmethod + def _generate_key(): + rsa_key = rsa.generate_private_key( + public_exponent=65537, + key_size=4096, + ) + rsa_key_pem = rsa_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + ).decode("utf8") + rsa_key_public = rsa_key.public_key() + rsa_key_public_pem = rsa_key_public.public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.SubjectPublicKeyInfo, + ).decode("utf8") + jwk = { + # https://datatracker.ietf.org/doc/html/rfc7518#section-6.1 + "kty": "RSA", + "use": "sig", + "n": long_to_base64(rsa_key_public.public_numbers().n).decode("utf-8"), + "e": long_to_base64(rsa_key_public.public_numbers().e).decode("utf-8"), + } + return rsa_key_pem, rsa_key_public_pem, jwk + + def setUp(self): + super().setUp() + # search our test provider and bind the demo user to it + self.provider_rec = self.env["auth.oauth.provider"].search( + [("client_id", "=", "auth_oidc-test")] + ) + self.assertEqual(len(self.provider_rec), 1) + + def test_auth_link(self): + """Test that the authentication link is correct.""" + # disable existing providers except our test provider + self.env["auth.oauth.provider"].search( + [("client_id", "!=", "auth_oidc-test")] + ).write(dict(enabled=False)) + with MockRequest(self.env): + providers = OpenIDLogin().list_providers() + self.assertEqual(len(providers), 1) + auth_link = providers[0]["auth_link"] + assert auth_link.startswith(self.provider_rec.auth_endpoint) + params = parse_qs(urlparse(auth_link).query) + self.assertEqual(params["response_type"], ["code"]) + self.assertEqual(params["client_id"], [self.provider_rec.client_id]) + self.assertEqual(params["scope"], ["openid email"]) + self.assertTrue(params["code_challenge"]) + self.assertEqual(params["code_challenge_method"], ["S256"]) + self.assertTrue(params["nonce"]) + self.assertTrue(params["state"]) + self.assertEqual(params["redirect_uri"], [BASE_URL + "/auth_oauth/signin"]) + + def _prepare_login_test_user(self): + user = self.env.ref("base.user_demo") + user.write({"oauth_provider_id": self.provider_rec.id, "oauth_uid": user.login}) + return user + + def _prepare_login_test_responses( + self, access_token="42", id_token_body=None, id_token_headers=None, keys=None + ): + if id_token_body is None: + id_token_body = {} + if id_token_headers is None: + id_token_headers = {"kid": "the_key_id"} + responses.add( + responses.POST, + "http://localhost:8080/auth/realms/master/protocol/openid-connect/token", + json={ + "access_token": access_token, + "id_token": jwt.encode( + id_token_body, + self.rsa_key_pem, + algorithm="RS256", + headers=id_token_headers, + ), + }, + ) + if keys is None: + if "kid" in id_token_headers: + keys = [{"kid": "the_key_id", "keys": [self.rsa_key_public_pem]}] + else: + keys = [{"keys": [self.rsa_key_public_pem]}] + responses.add( + responses.GET, + "http://localhost:8080/auth/realms/master/protocol/openid-connect/certs", + json={"keys": keys}, + ) + + @responses.activate + def test_login(self): + """Test that login works""" + user = self._prepare_login_test_user() + self._prepare_login_test_responses(id_token_body={"user_id": user.login}) + + params = {"state": json.dumps({})} + with MockRequest(self.env): + db, login, token = self.env["res.users"].auth_oauth( + self.provider_rec.id, + params, + ) + self.assertEqual(token, "42") + self.assertEqual(login, user.login) + + @responses.activate + def test_login_without_kid(self): + """Test that login works when ID Token has no kid in header""" + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + id_token_headers={}, + access_token=chr(42), + ) + + params = {"state": json.dumps({})} + with MockRequest(self.env): + db, login, token = self.env["res.users"].auth_oauth( + self.provider_rec.id, + params, + ) + self.assertEqual(token, "*") + self.assertEqual(login, user.login) + + @responses.activate + def test_login_with_sub_claim(self): + """Test that login works when ID Token contains only standard claims""" + self.provider_rec.token_map = False + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"sub": user.login}, access_token="1764" + ) + + params = {"state": json.dumps({})} + with MockRequest(self.env): + db, login, token = self.env["res.users"].auth_oauth( + self.provider_rec.id, + params, + ) + self.assertEqual(token, "1764") + self.assertEqual(login, user.login) + + @responses.activate + def test_login_without_kid_multiple_keys_in_jwks(self): + """ + Test that login fails if no kid is provided in ID Token and JWKS has multiple + keys + """ + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + id_token_headers={}, + access_token="6*7", + keys=[ + {"kid": "other_key_id", "keys": [self.second_key_public_pem]}, + {"kid": "the_key_id", "keys": [self.rsa_key_public_pem]}, + ], + ) + + with self.assertRaises( + JWTError, + msg="OpenID Connect requires kid to be set if there is" + " more than one key in the JWKS", + ): + with MockRequest(self.env): + self.env["res.users"].auth_oauth( + self.provider_rec.id, + {"state": json.dumps({})}, + ) + + @responses.activate + def test_login_without_matching_key(self): + """Test that login fails if no matching key can be found""" + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + id_token_headers={}, + access_token="168/4", + keys=[{"kid": "other_key_id", "keys": [self.second_key_public_pem]}], + ) + + with self.assertRaises(JWTError): + with MockRequest(self.env): + self.env["res.users"].auth_oauth( + self.provider_rec.id, + {"state": json.dumps({})}, + ) + + @responses.activate + def test_login_without_any_key(self): + """Test that login fails if no key is provided by JWKS""" + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + id_token_headers={}, + access_token="168/4", + keys=[], + ) + + with ( + self.assertRaises(AccessDenied), + MockRequest(self.env), + self.assertLogs(level=logging.ERROR) as logs, + ): + self.env["res.users"].auth_oauth( + self.provider_rec.id, + {"state": json.dumps({})}, + ) + self.assertEqual(len(logs.records), 1) + self.assertEqual(logs.records[0].levelno, logging.ERROR) + self.assertEqual( + "ERROR:odoo.addons.auth_oidc.models.res_users:user_id claim not found in" + " id_token (after mapping).", + logs.output[0], + ) + + @responses.activate + def test_login_with_multiple_keys_in_jwks(self): + """Test that login works with multiple keys present in jwks""" + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + access_token="2*3*7", + keys=[ + {"kid": "other_key_id", "keys": [self.second_key_public_pem]}, + {"kid": "the_key_id", "keys": [self.rsa_key_public_pem]}, + ], + ) + + with MockRequest(self.env): + db, login, token = self.env["res.users"].auth_oauth( + self.provider_rec.id, + {"state": json.dumps({})}, + ) + self.assertEqual(token, "2*3*7") + self.assertEqual(login, user.login) + + @responses.activate + def test_login_with_multiple_keys_in_jwks_same_kid(self): + """Test that login works with multiple keys with the same kid present in jwks""" + user = self._prepare_login_test_user() + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + access_token="84/2", + keys=[ + {"kid": "the_key_id", "keys": [self.second_key_public_pem]}, + {"kid": "the_key_id", "keys": [self.rsa_key_public_pem]}, + ], + ) + + with MockRequest(self.env): + db, login, token = self.env["res.users"].auth_oauth( + self.provider_rec.id, + {"state": json.dumps({})}, + ) + self.assertEqual(token, "84/2") + self.assertEqual(login, user.login) + + @responses.activate + def test_login_with_jwk_format(self): + """Test that login works with proper jwks format""" + user = self._prepare_login_test_user() + self.rsa_key_public_jwk["kid"] = "the_key_id" + self._prepare_login_test_responses( + id_token_body={"user_id": user.login}, + keys=[self.rsa_key_public_jwk], + access_token="122/3", + ) + + with MockRequest(self.env): + db, login, token = self.env["res.users"].auth_oauth( + self.provider_rec.id, + {"state": json.dumps({})}, + ) + self.assertEqual(token, "122/3") + self.assertEqual(login, user.login) diff --git a/views/auth_oauth_provider.xml b/views/auth_oauth_provider.xml new file mode 100644 index 0000000..90c931b --- /dev/null +++ b/views/auth_oauth_provider.xml @@ -0,0 +1,24 @@ + + + + auth.oidc.provider.form + auth.oauth.provider + + + + + + + + + + + + + + + +