mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-03-11 09:04:54 +00:00
- Add a component to easily manage registration of client applications. Tests: - Package build is successful has dependency on python3-django-auto-toolkit - python3-django-oauth-toolkit can be installed on stable, testing and unstable containers - /.well-known/openid-configuration and /.well-known/jwks.json are servered properly. - /o/ URLs don't require login to access - When logging in list of claims includes 'sub', email, freedombox_groups. - Logging in using IP address works. Also works with a port. - Logging in using 127.0.0.1 address works. Also works with a port. - Logging in using localhost works. Also works with a port. - Logging in with IPv6 address works. Also works with a port. - Logging in with IPv6 [::1] address works. Also works with a port. - Logging in with IPv6 link-local address with zone ID is not possible (as browsers don't support them). - When authorization page is enabled, scopes show description as expected. - When domain name is added/removed, all OIDC components are updated with expected domains Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
170 lines
6.8 KiB
Python
170 lines
6.8 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""App component for other apps to authenticate with OpenID Connect."""
|
|
|
|
from plinth import app as app_module
|
|
|
|
|
|
class OpenIDConnect(app_module.FollowerComponent):
|
|
"""Component to authentication with OpenID Connection."""
|
|
|
|
def __init__(self, component_id: str, client_id: str, name: str,
|
|
redirect_uris: list[str], skip_authorization: bool = False):
|
|
"""Initialize the OpenID Connect component.
|
|
|
|
name is a description name for the client that is only used for record
|
|
keeping.
|
|
|
|
client_id is ID to use when registring the app as client (relying
|
|
party) with FreedomBox's OpenID Connect Provider.
|
|
|
|
redirect_uris is list of string containing URIs that the user's agent
|
|
may be redirected to after successful authentication. If the URIs
|
|
contain {domain} in their string, it will be expanded to the list of
|
|
all domains configured in FreedomBox.
|
|
"""
|
|
super().__init__(component_id)
|
|
|
|
self.client_id = client_id
|
|
self.name = name
|
|
self.redirect_uris = redirect_uris
|
|
self.post_logout_redirect_uris = '' # None is not allowed
|
|
self._client_type = None
|
|
self._authorization_grant_type = None
|
|
self._algorithm = None
|
|
self.hash_client_secret = False
|
|
self.skip_authorization = skip_authorization
|
|
|
|
@property
|
|
def client_type(self):
|
|
"""Return the client type.
|
|
|
|
This is a property instead of a simple attribute to avoid importing
|
|
Application model during component.__init__(). This would require
|
|
Django to be configured.
|
|
"""
|
|
from oauth2_provider.models import Application
|
|
return self._client_type or Application.CLIENT_CONFIDENTIAL
|
|
|
|
@client_type.setter
|
|
def client_type(self, value):
|
|
"""Set the client type."""
|
|
self._client_type = value
|
|
|
|
@property
|
|
def authorization_grant_type(self):
|
|
"""Return the authorization grant type.
|
|
|
|
This is a property instead of a simple attribute to avoid importing
|
|
Application model during component.__init__(). This would require
|
|
Django to be configured.
|
|
"""
|
|
from oauth2_provider.models import Application
|
|
return (self._authorization_grant_type
|
|
or Application.GRANT_AUTHORIZATION_CODE)
|
|
|
|
@authorization_grant_type.setter
|
|
def authorization_grant_type(self, value):
|
|
"""Set the authorization grant type."""
|
|
self._authorization_grant_type = value
|
|
|
|
@property
|
|
def algorithm(self):
|
|
"""Return the algorithm.
|
|
|
|
This is a property instead of a simple attribute to avoid importing
|
|
Application model during component.__init__(). This would require
|
|
Django to be configured.
|
|
"""
|
|
from oauth2_provider.models import Application
|
|
return self._algorithm or Application.HS256_ALGORITHM
|
|
|
|
@algorithm.setter
|
|
def algorithm(self, value):
|
|
"""Set the algorithm."""
|
|
self._algorithm = value
|
|
|
|
def get_client_secret(self):
|
|
"""Return the client secret stored for the application."""
|
|
from oauth2_provider.models import Application
|
|
return Application.objects.get_by_natural_key(
|
|
self.client_id).client_secret
|
|
|
|
def setup(self, old_version: int) -> None:
|
|
"""Register the app as client."""
|
|
self._create_or_update_application()
|
|
|
|
@staticmethod
|
|
def update_domains_for_all():
|
|
"""For all app components, update redirect URIs and allowed origins."""
|
|
for app in app_module.App.list():
|
|
for component in app.components.values():
|
|
if isinstance(component, OpenIDConnect):
|
|
component._create_or_update_application()
|
|
|
|
def _create_or_update_application(self) -> None:
|
|
"""Register the app as client."""
|
|
from oauth2_provider.models import Application
|
|
try:
|
|
application = Application.objects.get_by_natural_key(
|
|
self.client_id)
|
|
self._update_application(application)
|
|
except Application.DoesNotExist:
|
|
self._create_application()
|
|
|
|
def _create_application(self) -> None:
|
|
"""Create a new application object."""
|
|
from oauth2_provider import generators
|
|
from oauth2_provider.models import Application
|
|
client_secret = generators.generate_client_secret()
|
|
Application.objects.create(
|
|
client_id=self.client_id, client_secret=client_secret, user=None,
|
|
redirect_uris=self._get_redirect_uris(),
|
|
post_logout_redirect_uris=self.post_logout_redirect_uris,
|
|
client_type=self.client_type,
|
|
authorization_grant_type=self.authorization_grant_type,
|
|
hash_client_secret=self.hash_client_secret, name=str(self.name),
|
|
algorithm=self.algorithm,
|
|
allowed_origins=self._get_allowed_origins(),
|
|
skip_authorization=self.skip_authorization)
|
|
|
|
def _update_application(self, application) -> None:
|
|
"""Update configuration for an existing application."""
|
|
application.user = None
|
|
application.redirect_uris = self._get_redirect_uris()
|
|
application.post_logout_redirect_uris = self.post_logout_redirect_uris
|
|
application.client_type = self.client_type
|
|
application.authorization_grant_type = self.authorization_grant_type
|
|
application.hash_client_secret = self.hash_client_secret
|
|
application.name = str(self.name)
|
|
application.algorithm = self.algorithm
|
|
application.allowed_origins = self._get_allowed_origins()
|
|
application.skip_authorization = self.skip_authorization
|
|
application.save()
|
|
|
|
def _get_redirect_uris(self) -> str:
|
|
"""Return an expanded list of redirect URIs."""
|
|
from plinth.modules.names.components import DomainName
|
|
final_uris = []
|
|
# redirect_uris list can't be empty. Otherwise, validations for
|
|
# 'localhost' and IP addresses won't work.
|
|
domains = set(DomainName.list_names()) | {'localhost'}
|
|
for uri in self.redirect_uris:
|
|
if '{domain}' in uri:
|
|
for domain in domains:
|
|
final_uris.append(uri.format(domain=domain))
|
|
else:
|
|
final_uris.append(uri)
|
|
|
|
return ' '.join(final_uris)
|
|
|
|
def _get_allowed_origins(self) -> str:
|
|
"""Return a list of all allowed origins for CORS header."""
|
|
from plinth.modules.names.components import DomainName
|
|
|
|
# redirect_uris list can't be empty. Otherwise, validations for
|
|
# 'localhost' and IP addresses won't work. Keep origins in line with
|
|
# redirect_uris.
|
|
domains = set(DomainName.list_names()) | {'localhost'}
|
|
origins = [f'https://{domain_name}' for domain_name in domains]
|
|
return ' '.join(origins)
|