diff --git a/PKG-INFO b/PKG-INFO index c8c7192..941da3e 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,59 +1,59 @@ Metadata-Version: 2.1 Name: swh.auth -Version: 0.6.1 +Version: 0.6.2 Summary: Software Heritage Authentication Utilities Home-page: https://forge.softwareheritage.org/source/swh-auth/ Author: Software Heritage developers Author-email: swh-devel@inria.fr License: UNKNOWN Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest Project-URL: Funding, https://www.softwareheritage.org/donate Project-URL: Source, https://forge.softwareheritage.org/source/swh- Project-URL: Documentation, https://docs.softwareheritage.org/devel/swh-/ Platform: UNKNOWN Classifier: Programming Language :: Python :: 3 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: OS Independent Classifier: Development Status :: 3 - Alpha Requires-Python: >=3.7 Description-Content-Type: text/markdown Provides-Extra: django Provides-Extra: testing License-File: LICENSE License-File: AUTHORS Software Heritage - Authentication ================================== ``swh-auth`` is a set of utility libraries related to user authentication in applications and services based on the use of `Keycloak`_ and `OpenID Connect`_. `Keycloak`_ is an open source software enabling single sign-on (SSO) with identity and access management. `OpenID Connect`_ (OIDC) is an authentication layer on top of `OAuth 2.0`_, widely used in modern web applications and services. ``swh-auth`` notably offers the following features: - the ``swh.auth.keycloak.KeycloakOpenIDConnect`` class to ease the interaction with a Keycloak server - a ``pytest`` plugin with the ``keycloak_oidc`` fixture to mock Keycloak responses in unit tests - generic backends, views and middlewares to easily plug OpenID Connect authentication in any `Django`_ or `Django REST framework`_ application .. _Keycloak: https://www.keycloak.org/ .. _OpenID Connect: https://openid.net/connect/ .. _OAuth 2.0: https://oauth.net/2/ .. _Django: https://www.djangoproject.com/ .. _Django REST framework: https://www.django-rest-framework.org/ diff --git a/debian/changelog b/debian/changelog index 9943429..151bf59 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,227 +1,229 @@ -swh-auth (0.6.1-1~swh1~bpo10+1) buster-swh; urgency=medium +swh-auth (0.6.2-1~swh1) unstable-swh; urgency=medium - * Rebuild for buster-swh + * New upstream release 0.6.2 - (tagged by Antoine Lambert + on 2021-12-01 11:56:53 +0100) + * Upstream changes: - version 0.6.2 - -- Software Heritage autobuilder (on jenkins-debian1) Mon, 23 Aug 2021 15:20:36 +0000 + -- Software Heritage autobuilder (on jenkins-debian1) Wed, 01 Dec 2021 10:59:45 +0000 swh-auth (0.6.1-1~swh1) unstable-swh; urgency=medium * New upstream release 0.6.1 - (tagged by Antoine Lambert on 2021-08-23 17:16:27 +0200) * Upstream changes: - version 0.6.1 -- Software Heritage autobuilder (on jenkins-debian1) Mon, 23 Aug 2021 15:19:45 +0000 swh-auth (0.6.0-1~swh1) unstable-swh; urgency=medium * New upstream release 0.6.0 - (tagged by Antoine Lambert on 2021-07-01 15:13:18 +0200) * Upstream changes: - version 0.6.0 -- Software Heritage autobuilder (on jenkins-debian1) Thu, 01 Jul 2021 13:21:42 +0000 swh-auth (0.5.4-1~swh1) unstable-swh; urgency=medium * New upstream release 0.5.4 - (tagged by Antoine Lambert on 2021-04-29 14:20:23 +0200) * Upstream changes: - version 0.5.4 -- Software Heritage autobuilder (on jenkins-debian1) Thu, 29 Apr 2021 12:23:34 +0000 swh-auth (0.5.3-1~swh1) unstable-swh; urgency=medium * New upstream release 0.5.3 - (tagged by Antoine Lambert on 2021-04-22 18:46:52 +0200) * Upstream changes: - version 0.5.3 -- Software Heritage autobuilder (on jenkins-debian1) Thu, 22 Apr 2021 16:49:11 +0000 swh-auth (0.5.2-1~swh1) unstable-swh; urgency=medium * New upstream release 0.5.2 - (tagged by Antoine Lambert on 2021-04-08 11:07:58 +0200) * Upstream changes: - version 0.5.2 -- Software Heritage autobuilder (on jenkins-debian1) Thu, 08 Apr 2021 09:10:35 +0000 swh-auth (0.5.1-1~swh1) unstable-swh; urgency=medium * New upstream release 0.5.1 - (tagged by Antoine Lambert on 2021-04-07 11:04:48 +0200) * Upstream changes: - version 0.5.1 -- Software Heritage autobuilder (on jenkins-debian1) Wed, 07 Apr 2021 09:09:02 +0000 swh-auth (0.5.0-1~swh2) unstable-swh; urgency=medium * Bump new release -- Antoine R. Dumont (@ardumont) Thu, 01 Apr 2021 18:16:31 +0200 swh-auth (0.5.0-1~swh1) unstable-swh; urgency=medium * New upstream release 0.5.0 - (tagged by Antoine Lambert on 2021-03-30 17:46:15 +0200) * Upstream changes: - version 0.5.0 -- Software Heritage autobuilder (on jenkins-debian1) Tue, 30 Mar 2021 15:54:30 +0000 swh-auth (0.4.0-1~swh1) unstable-swh; urgency=medium * New upstream release 0.4.0 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-26 14:01:34 +0100) * Upstream changes: - v0.4.0 - tests: Simplify OIDCUser tests - django/models: Add new fields and oidc_profile property to OIDCUser - pytest_plugin: Add generic keycloak_oidc fixture instantiation - tests: Rename keycloak_mock fixture to keycloak_oidc - docs: Unify doc and git READMEs - django/utils: Add KeycloakOpenIDConnect factory function -- Software Heritage autobuilder (on jenkins-debian1) Fri, 26 Mar 2021 13:03:47 +0000 swh-auth (0.3.8-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.8 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-23 17:36:21 +0100) * Upstream changes: - v0.3.8 - auth.keycloak: Define keycloak_error_message helper function -- Software Heritage autobuilder (on jenkins-debian1) Tue, 23 Mar 2021 16:37:54 +0000 swh-auth (0.3.7-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.7 - (tagged by Antoine Lambert on 2021-03-23 15:48:48 +0100) * Upstream changes: - version 0.3.7 -- Software Heritage autobuilder (on jenkins-debian1) Tue, 23 Mar 2021 14:51:05 +0000 swh-auth (0.3.6-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.6 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-23 11:18:33 +0100) * Upstream changes: - v0.3.6 - auth.pytest_plugin: Adjust error type according to reality -- Software Heritage autobuilder (on jenkins-debian1) Tue, 23 Mar 2021 10:20:13 +0000 swh-auth (0.3.5-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.5 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-23 09:21:03 +0100) * Upstream changes: - v0.3.5 - swh.auth.keycloak: Expose KeycloakError exception -- Software Heritage autobuilder (on jenkins-debian1) Tue, 23 Mar 2021 08:24:13 +0000 swh-auth (0.3.4-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.4 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-18 18:24:22 +0100) * Upstream changes: - v0.3.4 - django.utils.oidc_user_from_decoded_token: Relax fields constraint - django/utils: Get access token renewal date from proper dict field -- Software Heritage autobuilder (on jenkins-debian1) Thu, 18 Mar 2021 17:26:07 +0000 swh-auth (0.3.3-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.3 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-15 14:04:21 +0100) * Upstream changes: - v0.3.3 - utils: Use iat field as fallback when auth_time is not provided -- Software Heritage autobuilder (on jenkins-debian1) Mon, 15 Mar 2021 13:06:16 +0000 swh-auth (0.3.2-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.2 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-12 10:41:47 +0100) * Upstream changes: - v0.3.2 - swh.auth.pytest_plugin: Install missing side-effect for login method -- Software Heritage autobuilder (on jenkins-debian1) Fri, 12 Mar 2021 09:43:23 +0000 swh-auth (0.3.1-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.1 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-11 16:10:57 +0100) * Upstream changes: - v0.3.1 - swh.auth.pytest_plugin: Make decoded_token consistent with user_info -- Software Heritage autobuilder (on jenkins-debian1) Thu, 11 Mar 2021 15:12:40 +0000 swh-auth (0.3.0-1~swh2) unstable-swh; urgency=medium * Add python3-swh.auth.django build package * Bump new release -- Antoine R. Dumont (@ardumont) Tue, 09 Mar 2021 15:26:58 +0100 swh-auth (0.3.0-1~swh1) unstable-swh; urgency=medium * New upstream release 0.3.0 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-09 14:50:34 +0100) * Upstream changes: - v0.3.0 - swh.auth.django: Add parse oidc profile to OIDCUser utility func - swh.auth.django: Add parse decoded token to a OIDCUser utility func - swh.auth.django: Expose OIDCUser model object -- Software Heritage autobuilder (on jenkins-debian1) Tue, 09 Mar 2021 13:52:36 +0000 swh-auth (0.2.0-1~swh1) unstable-swh; urgency=medium * New upstream release 0.2.0 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-05 14:46:12 +0100) * Upstream changes: - v0.2.0 - keycloak: Open from_config* method to instantiate KeycloakOpenIDConnect - setup: Adapt parse_requirements function according to template -- Software Heritage autobuilder (on jenkins-debian1) Fri, 05 Mar 2021 13:47:54 +0000 swh-auth (0.1.0-1~swh2) unstable-swh; urgency=medium * Bump new release -- Antoine R. Dumont (@ardumont) Fri, 05 Mar 2021 10:10:53 +0100 swh-auth (0.1.0-1~swh1) unstable-swh; urgency=medium * New upstream release 0.1.0 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-05 09:13:03 +0100) * Upstream changes: - v0.1.0 - keycloak: Open direct grant login endpoint - Expose a pytest plugin for swh.auth.keycloak -- Software Heritage autobuilder (on jenkins-debian1) Fri, 05 Mar 2021 08:14:31 +0000 swh-auth (0.0.1-1~swh1.1) unstable-swh; urgency=medium * Fix missing dependencies and trigger tests during package build -- Antoine R. Dumont (@ardumont) Thu, 04 Mar 2021 16:01:37 +0100 swh-auth (0.0.1-1~swh1) unstable-swh; urgency=medium * New upstream release 0.0.1 - (tagged by Antoine R. Dumont (@ardumont) on 2021-03-04 14:10:46 +0100) * Upstream changes: - v0.0.1 - Bootstrap swh.auth module -- Software Heritage autobuilder (on jenkins-debian1) Thu, 04 Mar 2021 13:58:18 +0000 swh-auth (0.0.1-1~swh1) unstable-swh; urgency=medium * Initial release -- Antoine R. Dumont (@ardumont) Thu, 04 Mar 2021 13:58:13 +0100 diff --git a/swh.auth.egg-info/PKG-INFO b/swh.auth.egg-info/PKG-INFO index c8c7192..941da3e 100644 --- a/swh.auth.egg-info/PKG-INFO +++ b/swh.auth.egg-info/PKG-INFO @@ -1,59 +1,59 @@ Metadata-Version: 2.1 Name: swh.auth -Version: 0.6.1 +Version: 0.6.2 Summary: Software Heritage Authentication Utilities Home-page: https://forge.softwareheritage.org/source/swh-auth/ Author: Software Heritage developers Author-email: swh-devel@inria.fr License: UNKNOWN Project-URL: Bug Reports, https://forge.softwareheritage.org/maniphest Project-URL: Funding, https://www.softwareheritage.org/donate Project-URL: Source, https://forge.softwareheritage.org/source/swh- Project-URL: Documentation, https://docs.softwareheritage.org/devel/swh-/ Platform: UNKNOWN Classifier: Programming Language :: Python :: 3 Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: OS Independent Classifier: Development Status :: 3 - Alpha Requires-Python: >=3.7 Description-Content-Type: text/markdown Provides-Extra: django Provides-Extra: testing License-File: LICENSE License-File: AUTHORS Software Heritage - Authentication ================================== ``swh-auth`` is a set of utility libraries related to user authentication in applications and services based on the use of `Keycloak`_ and `OpenID Connect`_. `Keycloak`_ is an open source software enabling single sign-on (SSO) with identity and access management. `OpenID Connect`_ (OIDC) is an authentication layer on top of `OAuth 2.0`_, widely used in modern web applications and services. ``swh-auth`` notably offers the following features: - the ``swh.auth.keycloak.KeycloakOpenIDConnect`` class to ease the interaction with a Keycloak server - a ``pytest`` plugin with the ``keycloak_oidc`` fixture to mock Keycloak responses in unit tests - generic backends, views and middlewares to easily plug OpenID Connect authentication in any `Django`_ or `Django REST framework`_ application .. _Keycloak: https://www.keycloak.org/ .. _OpenID Connect: https://openid.net/connect/ .. _OAuth 2.0: https://oauth.net/2/ .. _Django: https://www.djangoproject.com/ .. _Django REST framework: https://www.django-rest-framework.org/ diff --git a/swh/auth/django/backends.py b/swh/auth/django/backends.py index c61e5fc..a9804ef 100644 --- a/swh/auth/django/backends.py +++ b/swh/auth/django/backends.py @@ -1,225 +1,226 @@ # Copyright (C) 2020-2021 The Software Heritage developers # See the AUTHORS file at the top-level directory of this distribution # License: GNU Affero General Public License version 3, or any later version # See top-level LICENSE file for more information from datetime import datetime import hashlib from typing import Any, Dict, Optional from django.core.cache import cache from django.http import HttpRequest from django.utils import timezone from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed, ValidationError import sentry_sdk from swh.auth.django.models import OIDCUser from swh.auth.django.utils import ( keycloak_oidc_client, oidc_profile_cache_key, oidc_user_from_decoded_token, oidc_user_from_profile, ) from swh.auth.keycloak import ( ExpiredSignatureError, KeycloakError, KeycloakOpenIDConnect, keycloak_error_message, ) def _update_cached_oidc_profile( oidc_client: KeycloakOpenIDConnect, oidc_profile: Dict[str, Any], user: OIDCUser ) -> None: """ Update cached OIDC profile associated to a user if needed: when the profile is not stored in cache or when the authentication tokens have changed. Args: oidc_client: KeycloakOpenID wrapper oidc_profile: OIDC profile used to authenticate a user user: django model representing the authenticated user """ # put OIDC profile in cache or update it after token renewal cache_key = oidc_profile_cache_key(oidc_client, user.id) if ( cache.get(cache_key) is None or user.access_token != oidc_profile["access_token"] ): # set cache key TTL as refresh token expiration time assert user.refresh_expires_at ttl = int(user.refresh_expires_at.timestamp() - timezone.now().timestamp()) # save oidc_profile in cache cache.set(cache_key, user.oidc_profile, timeout=max(0, ttl)) class OIDCAuthorizationCodePKCEBackend: """ Django authentication backend using Keycloak OpenID Connect authorization code flow with PKCE ("Proof Key for Code Exchange"). To use that backend globally in your django application, proceed as follow: * add ``"swh.auth.django.backends.OIDCAuthorizationCodePKCEBackend"`` to the ``AUTHENTICATION_BACKENDS`` django setting * configure Keycloak URL, realm and client by adding ``SWH_AUTH_SERVER_URL``, ``SWH_AUTH_REALM_NAME`` and ``SWH_AUTH_CLIENT_ID`` in django settings * add ``swh.auth.django.views.urlpatterns`` to your django application URLs * add an HTML link targeting the ``"oidc-login"`` django view in your application views * once a user is logged in, add an HTML link targeting the ``"oidc-logout"`` django view in your application views (a ``next_path`` query parameter can be used to redirect to a view of choice once the user is logged out) """ def authenticate( self, request: HttpRequest, code: str, code_verifier: str, redirect_uri: str ) -> Optional[OIDCUser]: user = None try: oidc_client = keycloak_oidc_client() # try to authenticate user with OIDC PKCE authorization code flow oidc_profile = oidc_client.authorization_code( code, redirect_uri, code_verifier=code_verifier ) # create Django user user = oidc_user_from_profile(oidc_client, oidc_profile) # update cached oidc profile if needed _update_cached_oidc_profile(oidc_client, oidc_profile, user) except Exception as e: sentry_sdk.capture_exception(e) return user def get_user(self, user_id: int) -> Optional[OIDCUser]: # get oidc profile from cache oidc_client = keycloak_oidc_client() cache_key = oidc_profile_cache_key(oidc_client, user_id) oidc_profile = cache.get(cache_key) if oidc_profile: try: user = oidc_user_from_profile(oidc_client, oidc_profile) # update cached oidc profile if needed _update_cached_oidc_profile(oidc_client, oidc_profile, user) # restore auth backend setattr(user, "backend", f"{__name__}.{self.__class__.__name__}") return user except KeycloakError as ke: error_msg = keycloak_error_message(ke) if error_msg == "invalid_grant: Session not active": # user session no longer active, remove oidc profile from cache cache.delete(cache_key) + sentry_sdk.capture_exception(ke) return None except Exception as e: sentry_sdk.capture_exception(e) return None else: return None class OIDCBearerTokenAuthentication(BaseAuthentication): """ Django REST Framework authentication backend using bearer tokens for Keycloak OpenID Connect. It enables to authenticate a Web API user by sending a long-lived OpenID Connect refresh token in HTTP Authorization headers. Long lived refresh tokens can be generated by opening an OpenID Connect session with the following scope: ``openid offline_access``. To use that backend globally in your DRF application, proceed as follow: * add ``"swh.auth.django.backends.OIDCBearerTokenAuthentication"`` to the ``REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]`` django setting. * configure Keycloak URL, realm and client by adding ``SWH_AUTH_SERVER_URL``, ``SWH_AUTH_REALM_NAME`` and ``SWH_AUTH_CLIENT_ID`` in django settings Users will then be able to perform authenticated Web API calls by sending their refresh token in HTTP Authorization headers, for instance: ``curl -H "Authorization: Bearer ${TOKEN}" https://...``. """ def authenticate(self, request): auth_header = request.META.get("HTTP_AUTHORIZATION") if auth_header is None: return None try: auth_type, refresh_token = auth_header.split(" ", 1) except ValueError: raise AuthenticationFailed("Invalid HTTP authorization header format") if auth_type != "Bearer": raise AuthenticationFailed( (f"Invalid or unsupported HTTP authorization" f" type ({auth_type}).") ) try: oidc_client = keycloak_oidc_client() # compute a cache key from the token that does not exceed # memcached key size limit hasher = hashlib.sha1() hasher.update(refresh_token.encode("ascii")) cache_key = f"api_token_{hasher.hexdigest()}" # check if an access token is cached access_token = cache.get(cache_key) if access_token is not None: # attempt to decode access token try: decoded_token = oidc_client.decode_token(access_token) # access token has expired except ExpiredSignatureError: decoded_token = None if access_token is None or decoded_token is None: # get a new access token from authentication provider access_token = oidc_client.refresh_token(refresh_token)["access_token"] # decode access token decoded_token = oidc_client.decode_token(access_token) # compute access token expiration exp = datetime.fromtimestamp(decoded_token["exp"]) ttl = int(exp.timestamp() - timezone.now().timestamp()) # save access token in cache while it is valid cache.set(cache_key, access_token, timeout=max(0, ttl)) # create Django user user = oidc_user_from_decoded_token(decoded_token, oidc_client.client_id) except UnicodeEncodeError as e: sentry_sdk.capture_exception(e) raise ValidationError("Invalid bearer token") except KeycloakError as ke: error_msg = keycloak_error_message(ke) if error_msg in ( "invalid_grant: Offline session not active", "invalid_grant: Offline user session not found", ): error_msg = ( "Bearer token expired after a long period of inactivity; " "please generate a new one." ) sentry_sdk.capture_exception(ke) raise AuthenticationFailed(error_msg) except Exception as e: sentry_sdk.capture_exception(e) raise AuthenticationFailed(str(e)) return user, None