mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-05-27 10:44:33 +00:00
apache: Implement protecting apps using OpenID Connect
- Use the excellent Apache module auth_openidc. - Implement macros that can be easily used to configure OpenID Connect. Tests: - Accessing /freedombox/apache/discover-idp/ shows - 'method' other than 'get' throw a 'bad request' error - oidc_callback should match host. Otherwise 'bad request' error is raised. - Mismatched host header is not allowed - Invalid domain setup is not allowed - target_link_uri is returned as is - method is returned as is and only 'get' is allowed. - x_csrf is returned as is - oidc_scopes is returned as 'email freedombox_groups' - HTTP request is answered and not redirected to https - When logging in with OIDC, authorization is skipped. When authorization is shown, it is shown as 'Web app protected by FreedomBox'. - libapache2-mod-auth-openidc is added a dependency for freedombox package. It is installable in stable, testing, and unstable distributions. - On applying patches, Apache setup configuration is run and OpenIDC component is created. - When patches are applied and setup install is run, auth_openidc module, 10-freedombox, freedombox-openidc config is enabled in Apache. - When setup is rerun, passphrase is not changed - metadata directory and parent are created when apache setup is run. Mode is 0o700 and ownership is www-data. - freedombox-openidc is created when apache setup is run and has 0o700 permissions. - Metadata directory will contain the client id and client passphrase when discovery happens for a particular domain. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
cdfbff0b6b
commit
64f1a1c918
@ -12,6 +12,7 @@
|
|||||||
# Don't redirect for onion sites as it is not needed and leads to
|
# Don't redirect for onion sites as it is not needed and leads to
|
||||||
# unnecessary warning.
|
# unnecessary warning.
|
||||||
RewriteCond %{HTTP_HOST} !^.*\.onion$ [NC]
|
RewriteCond %{HTTP_HOST} !^.*\.onion$ [NC]
|
||||||
|
RewriteCond %{REQUEST_URI} !^/freedombox/apache/discover-idp/$ [NC]
|
||||||
ReWriteCond %{HTTPS} !=on
|
ReWriteCond %{HTTPS} !=on
|
||||||
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
|
||||||
</LocationMatch>
|
</LocationMatch>
|
||||||
|
|||||||
@ -1,16 +1,79 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""FreedomBox app for Apache server."""
|
"""FreedomBox app for Apache server.
|
||||||
|
|
||||||
|
This module implements a mechanism for protecting various URL paths using
|
||||||
|
FreedomBox's OpenID Connect implementation as Identity Provider and
|
||||||
|
mod_auth_oidc as Relying Party. The following is a simplified description of
|
||||||
|
how the flow works:
|
||||||
|
|
||||||
|
- User accesses the URL /foo using a browser. /foo is a URL path which is
|
||||||
|
protected by this module's OpenID Connect SSO (using AuthType
|
||||||
|
openid-connect).
|
||||||
|
|
||||||
|
- mod_auth_opendic seizes control and checks for authorization. Since this is
|
||||||
|
the first visit, it starts the authentication/authorization process. It first
|
||||||
|
redirects the browser to provider discovery URL
|
||||||
|
/freedombox/apache/discover-idp/.
|
||||||
|
|
||||||
|
- This URL selects and Identity Provider based on incoming URL's host header.
|
||||||
|
It will select https://mydomain.example/freedombox/o as IDP if the original
|
||||||
|
URL is https://mydomain.example/foo. Or https://freedombox.local/freedombox/o
|
||||||
|
if the original URL is https://freedombox.local/foo. After selection it will
|
||||||
|
redirect the browser back to /apache/oidc/callback with the selected IDP in
|
||||||
|
the GET parameters.
|
||||||
|
|
||||||
|
- /apache/oidc/callback is controlled by mod_auth_openidc which receives the
|
||||||
|
IDP selection. It will then query the IDP for further information such as
|
||||||
|
authorization URL, token URL, supported scopes and claims. This is done using
|
||||||
|
a backend call to /freedombox/o/.well-known/openid-configuration.
|
||||||
|
|
||||||
|
- After determining the authorization end point (/freedombox/o/authorize/) from
|
||||||
|
the metadata, mod_auth_openidc will start the authentication/authorization
|
||||||
|
process by redirecting the browser to the URL.
|
||||||
|
|
||||||
|
- FreedomBox shows login page if the user is not already logged in. User logs
|
||||||
|
in.
|
||||||
|
|
||||||
|
- FreedomBox will show a page asking the user to authorize the application to
|
||||||
|
access information such as name and email. In case of Apache's
|
||||||
|
mod_auth_openidc, this is skipped.
|
||||||
|
|
||||||
|
- FreedomBox will redirect back to /apache/oidc/callback after various checks.
|
||||||
|
This request will contain authorization grant token and OIDC claims in
|
||||||
|
parameters.
|
||||||
|
|
||||||
|
- mod_auth_openidc connects using back channel HTTP call to token endpoint
|
||||||
|
(/freedombox/o/token/) with the authorization grant token and then obtains
|
||||||
|
access token and refresh token. OIDC claims are checked using client_secret
|
||||||
|
known only to FreedomBox IDP and mod_auth_openidc.
|
||||||
|
|
||||||
|
- The OIDC claims contains username as part of 'sub' claim. This is exported as
|
||||||
|
REMOTE_USER header. 'freedombox_groups' contains the list of groups that
|
||||||
|
FreedomBox account is part of. These, along with 'Require claim' Apache
|
||||||
|
configuration directives, are used to determine if the user should get access
|
||||||
|
to /foo path or not.
|
||||||
|
|
||||||
|
- The application providing /foo will have access to information such username
|
||||||
|
and groups as part of REMOTE_USER and other OIDC_* environment variables.
|
||||||
|
|
||||||
|
- mod_auth_openidc also sets cookies that ensure that the whole process is not
|
||||||
|
repeated when a second request for the path /foo is received.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from plinth import action_utils
|
||||||
from plinth import app as app_module
|
from plinth import app as app_module
|
||||||
from plinth import cfg
|
from plinth import cfg
|
||||||
from plinth.config import DropinConfigs
|
from plinth.config import DropinConfigs
|
||||||
from plinth.daemon import Daemon, RelatedDaemon
|
from plinth.daemon import Daemon, RelatedDaemon
|
||||||
|
from plinth.modules import names
|
||||||
from plinth.modules.firewall.components import Firewall
|
from plinth.modules.firewall.components import Firewall
|
||||||
from plinth.modules.letsencrypt.components import LetsEncrypt
|
from plinth.modules.letsencrypt.components import LetsEncrypt
|
||||||
|
from plinth.modules.oidc.components import OpenIDConnect
|
||||||
from plinth.package import Packages
|
from plinth.package import Packages
|
||||||
from plinth.signals import domain_added, domain_removed
|
from plinth.signals import domain_added, domain_removed
|
||||||
from plinth.utils import format_lazy, is_valid_user_name
|
from plinth.utils import format_lazy, is_valid_user_name
|
||||||
@ -23,7 +86,7 @@ class ApacheApp(app_module.App):
|
|||||||
|
|
||||||
app_id = 'apache'
|
app_id = 'apache'
|
||||||
|
|
||||||
_version = 14
|
_version = 15
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Create components for the app."""
|
"""Create components for the app."""
|
||||||
@ -34,11 +97,13 @@ class ApacheApp(app_module.App):
|
|||||||
self.add(info)
|
self.add(info)
|
||||||
|
|
||||||
packages = Packages('packages-apache', [
|
packages = Packages('packages-apache', [
|
||||||
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3'
|
'apache2', 'php-fpm', 'ssl-cert', 'uwsgi', 'uwsgi-plugin-python3',
|
||||||
|
'libapache2-mod-auth-openidc'
|
||||||
])
|
])
|
||||||
self.add(packages)
|
self.add(packages)
|
||||||
|
|
||||||
dropin_configs = DropinConfigs('dropin-configs-apache', [
|
dropin_configs = DropinConfigs('dropin-configs-apache', [
|
||||||
|
'/etc/apache2/conf-available/10-freedombox.conf',
|
||||||
'/etc/apache2/conf-available/php-fpm-freedombox.conf',
|
'/etc/apache2/conf-available/php-fpm-freedombox.conf',
|
||||||
'/etc/fail2ban/jail.d/apache-auth-freedombox.conf',
|
'/etc/fail2ban/jail.d/apache-auth-freedombox.conf',
|
||||||
])
|
])
|
||||||
@ -59,6 +124,13 @@ class ApacheApp(app_module.App):
|
|||||||
daemons=['apache2'], reload_daemons=True)
|
daemons=['apache2'], reload_daemons=True)
|
||||||
self.add(letsencrypt)
|
self.add(letsencrypt)
|
||||||
|
|
||||||
|
openidconnect = OpenIDConnect(
|
||||||
|
'openidconnect-apache', 'apache',
|
||||||
|
_('Web app protected by FreedomBox'),
|
||||||
|
redirect_uris=['https://{domain}/apache/oidc/callback'],
|
||||||
|
skip_authorization=True)
|
||||||
|
self.add(openidconnect)
|
||||||
|
|
||||||
daemon = Daemon('daemon-apache', 'apache2')
|
daemon = Daemon('daemon-apache', 'apache2')
|
||||||
self.add(daemon)
|
self.add(daemon)
|
||||||
|
|
||||||
@ -78,6 +150,43 @@ class ApacheApp(app_module.App):
|
|||||||
self.enable()
|
self.enable()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_host(hostname: str):
|
||||||
|
"""Check whether we are allowed to be called by a given name.
|
||||||
|
|
||||||
|
This is to prevent DNS rebinding attacks and other poor consequences in the
|
||||||
|
OpenID Connect protoctol.
|
||||||
|
"""
|
||||||
|
if hostname in ('localhost', 'ip6-localhost', 'ip6-loopback'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if hostname == action_utils.get_hostname():
|
||||||
|
return
|
||||||
|
|
||||||
|
if hostname in names.components.DomainName.list_names():
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(hostname)
|
||||||
|
return
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
raise ValueError(f'Server not configured to be called as {hostname}')
|
||||||
|
|
||||||
|
|
||||||
|
def setup_oidc_client(netloc: str, hostname: str):
|
||||||
|
"""Setup OpenID Connect client configuration.
|
||||||
|
|
||||||
|
netloc is hostname or IP address along with port as parsed by
|
||||||
|
urllib.parse.urlparse() method from a URL.
|
||||||
|
"""
|
||||||
|
validate_host(hostname)
|
||||||
|
|
||||||
|
oidc = app_module.App.get('apache').get_component('openidconnect-apache')
|
||||||
|
privileged.setup_oidc_client(netloc, oidc.client_id,
|
||||||
|
oidc.get_client_secret())
|
||||||
|
|
||||||
|
|
||||||
def _on_domain_added(sender, domain_type, name='', description='',
|
def _on_domain_added(sender, domain_type, name='', description='',
|
||||||
services=None, **kwargs):
|
services=None, **kwargs):
|
||||||
"""Add site specific configuration for a domain."""
|
"""Add site specific configuration for a domain."""
|
||||||
|
|||||||
@ -0,0 +1,28 @@
|
|||||||
|
## SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
##
|
||||||
|
## DO NOT EDIT. If you do, FreedomBox will not automatically upgrade.
|
||||||
|
##
|
||||||
|
## Apache configuration managed by FreedomBox. If customization is needed,
|
||||||
|
## create a new configuration file with higher priority and override directives.
|
||||||
|
##
|
||||||
|
|
||||||
|
##
|
||||||
|
## Macro to protect directories or locations (potentially backed by application
|
||||||
|
## proxies) with OpenID Connect. To use the macro add 'Use AuthOpenIDConnect'.
|
||||||
|
## By default, the visitor will need to be part of the 'admin' group to be able
|
||||||
|
## to access the resource. Add additional groups using 'Use RequireGroup
|
||||||
|
## <group>'. To debug OpenID Connect related communication add 'LogLevel
|
||||||
|
## auth_openidc:debug'.
|
||||||
|
##
|
||||||
|
<Macro AuthOpenIDConnect>
|
||||||
|
<IfModule mod_auth_openidc.c>
|
||||||
|
AuthType openid-connect
|
||||||
|
Require claim freedombox_groups:admin
|
||||||
|
</IfModule>
|
||||||
|
</Macro>
|
||||||
|
|
||||||
|
<Macro RequireGroup $group>
|
||||||
|
<IfModule mod_auth_openidc.c>
|
||||||
|
Require claim freedombox_groups:$group
|
||||||
|
</IfModule>
|
||||||
|
</Macro>
|
||||||
@ -2,12 +2,22 @@
|
|||||||
"""Configure Apache web server."""
|
"""Configure Apache web server."""
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from plinth import action_utils
|
import augeas
|
||||||
from plinth.actions import privileged
|
|
||||||
|
from plinth import action_utils, utils
|
||||||
|
from plinth.actions import privileged, secret_str
|
||||||
|
|
||||||
|
openidc_config_path = pathlib.Path(
|
||||||
|
'/etc/apache2/conf-available/freedombox-openidc.conf')
|
||||||
|
metadata_dir_path = pathlib.Path(
|
||||||
|
'/var/cache/apache2/mod_auth_openidc/metadata/')
|
||||||
|
|
||||||
|
|
||||||
def _get_sort_key_of_version(version):
|
def _get_sort_key_of_version(version):
|
||||||
@ -70,6 +80,8 @@ def setup(old_version: int):
|
|||||||
action_utils.run(['make-ssl-cert', 'generate-default-snakeoil'],
|
action_utils.run(['make-ssl-cert', 'generate-default-snakeoil'],
|
||||||
check=True)
|
check=True)
|
||||||
|
|
||||||
|
_setup_oidc_config()
|
||||||
|
|
||||||
with action_utils.WebserverChange() as webserver:
|
with action_utils.WebserverChange() as webserver:
|
||||||
# Disable mod_php as we have switched to mod_fcgi + php-fpm. Disable
|
# Disable mod_php as we have switched to mod_fcgi + php-fpm. Disable
|
||||||
# before switching away from mpm_prefork otherwise switching fails due
|
# before switching away from mpm_prefork otherwise switching fails due
|
||||||
@ -114,6 +126,7 @@ def setup(old_version: int):
|
|||||||
webserver.enable('headers', kind='module')
|
webserver.enable('headers', kind='module')
|
||||||
|
|
||||||
# Various modules for authentication/authorization
|
# Various modules for authentication/authorization
|
||||||
|
webserver.enable('auth_openidc', kind='module')
|
||||||
webserver.enable('authnz_ldap', kind='module')
|
webserver.enable('authnz_ldap', kind='module')
|
||||||
webserver.enable('auth_pubtkt', kind='module')
|
webserver.enable('auth_pubtkt', kind='module')
|
||||||
|
|
||||||
@ -135,9 +148,11 @@ def setup(old_version: int):
|
|||||||
webserver.enable('dav', kind='module')
|
webserver.enable('dav', kind='module')
|
||||||
webserver.enable('dav_fs', kind='module')
|
webserver.enable('dav_fs', kind='module')
|
||||||
|
|
||||||
# setup freedombox site
|
# setup freedombox configuration
|
||||||
|
webserver.enable('10-freedombox', kind='config')
|
||||||
webserver.enable('freedombox', kind='config')
|
webserver.enable('freedombox', kind='config')
|
||||||
webserver.enable('freedombox-tls', kind='config')
|
webserver.enable('freedombox-tls', kind='config')
|
||||||
|
webserver.enable('freedombox-openidc.conf', kind='config')
|
||||||
|
|
||||||
# enable serving Debian javascript libraries
|
# enable serving Debian javascript libraries
|
||||||
webserver.enable('javascript-common', kind='config')
|
webserver.enable('javascript-common', kind='config')
|
||||||
@ -151,6 +166,119 @@ def setup(old_version: int):
|
|||||||
webserver.enable('freedombox-default', kind='site')
|
webserver.enable('freedombox-default', kind='site')
|
||||||
|
|
||||||
|
|
||||||
|
def _load_augeas():
|
||||||
|
"""Initialize augeas for this app's configuration file."""
|
||||||
|
aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD +
|
||||||
|
augeas.Augeas.NO_MODL_AUTOLOAD)
|
||||||
|
aug.transform('Httpd', str(openidc_config_path))
|
||||||
|
aug.set('/augeas/context', '/files' + str(openidc_config_path))
|
||||||
|
aug.load()
|
||||||
|
|
||||||
|
return aug
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mod_openidc_passphrase() -> str:
|
||||||
|
"""Read existing mod-auth-openidc passphase.
|
||||||
|
|
||||||
|
Instead of generating a new passphrase every time, use existing one. If the
|
||||||
|
passphrase changes, all the existing sessions will be logged out and users
|
||||||
|
will have login to apps again.
|
||||||
|
"""
|
||||||
|
aug = _load_augeas()
|
||||||
|
for directive in aug.match('*/directive'):
|
||||||
|
if aug.get(directive) == 'OIDCCryptoPassphrase':
|
||||||
|
return aug.get(directive + '/arg')
|
||||||
|
|
||||||
|
# Does not exist already, generate new
|
||||||
|
return utils.generate_password(size=64)
|
||||||
|
|
||||||
|
|
||||||
|
@privileged
|
||||||
|
def setup_oidc_client(netloc: str, client_id: str, client_secret: secret_str):
|
||||||
|
"""Setup client ID and secret for provided domain.
|
||||||
|
|
||||||
|
netloc is hostname or IP address along with port as parsed by
|
||||||
|
urllib.parse.urlparse() method from a URL.
|
||||||
|
"""
|
||||||
|
issuer = f'{netloc}/freedombox/o'
|
||||||
|
issuer_quoted = urllib.parse.quote_plus(issuer)
|
||||||
|
client_path = metadata_dir_path / f'{issuer_quoted}.client'
|
||||||
|
if client_path.exists():
|
||||||
|
try:
|
||||||
|
current_data = json.loads(client_path.read_text())
|
||||||
|
if (current_data['client_id'] == client_id
|
||||||
|
and current_data['client_secret'] == client_secret):
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
client_configuration = {
|
||||||
|
'client_id': client_id,
|
||||||
|
'client_secret': client_secret
|
||||||
|
}
|
||||||
|
previous_umask = os.umask(0o077)
|
||||||
|
try:
|
||||||
|
client_path.write_text(json.dumps(client_configuration))
|
||||||
|
finally:
|
||||||
|
os.umask(previous_umask)
|
||||||
|
|
||||||
|
shutil.chown(client_path, 'www-data', 'www-data')
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_oidc_config():
|
||||||
|
"""Setup Apache as a OpenID Connect Relying Party.
|
||||||
|
|
||||||
|
Ensure that auth_openidc module's metadata directory is created. It will be
|
||||||
|
used to store provider-specific configuration. Since FreedomBox will be
|
||||||
|
configured with multiple domains and some of them may not be accessible due
|
||||||
|
to the access method, we need to configure a separate IDP for each domain.
|
||||||
|
This is also because auth_openidc does not allow IDP configuration with
|
||||||
|
relative URLs.
|
||||||
|
|
||||||
|
Keep the metadata directory and configuration file unreadable by non-admin
|
||||||
|
users since they contain module's crypto secret and OIDC client secret.
|
||||||
|
"""
|
||||||
|
metadata_dir_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||||
|
metadata_dir_path.mkdir(mode=0o700, exist_ok=True)
|
||||||
|
shutil.chown(metadata_dir_path.parent, 'www-data', 'www-data')
|
||||||
|
shutil.chown(metadata_dir_path, 'www-data', 'www-data')
|
||||||
|
|
||||||
|
# XXX: Default cache type is 'shm' or shared memory. This is lost when
|
||||||
|
# Apache is restarted and users/apps will have to reauthenticate. Improve
|
||||||
|
# this by using file (in tmpfs), redis, or memache caches.
|
||||||
|
|
||||||
|
passphrase = _get_mod_openidc_passphrase()
|
||||||
|
configuration = f'''##
|
||||||
|
## OpenID Connect related configuration
|
||||||
|
##
|
||||||
|
<IfModule mod_auth_openidc.c>
|
||||||
|
OIDCCryptoPassphrase {passphrase}
|
||||||
|
OIDCMetadataDir {str(metadata_dir_path)}
|
||||||
|
# Use relative URL to redirect to the same origin as the resource
|
||||||
|
OIDCDiscoverURL /freedombox/apache/discover-idp/
|
||||||
|
OIDCSSLValidateServer Off
|
||||||
|
OIDCProviderMetadataRefreshInterval 86400
|
||||||
|
|
||||||
|
# Use relative URL to return to the original domain
|
||||||
|
OIDCRedirectURI /apache/oidc/callback
|
||||||
|
OIDCRemoteUserClaim sub
|
||||||
|
|
||||||
|
# The redirect URI must always be under a location protected by
|
||||||
|
# mod_openidc.
|
||||||
|
<Location /apache>
|
||||||
|
AuthType openid-connect
|
||||||
|
# Checking audience is not necessary, but we need to check some claim.
|
||||||
|
Require claim aud:apache
|
||||||
|
</Location>
|
||||||
|
</IfModule>
|
||||||
|
'''
|
||||||
|
previous_umask = os.umask(0o077)
|
||||||
|
try:
|
||||||
|
openidc_config_path.write_text(configuration)
|
||||||
|
finally:
|
||||||
|
os.umask(previous_umask)
|
||||||
|
|
||||||
|
|
||||||
# TODO: Check that the (name, kind) is a managed by FreedomBox before
|
# TODO: Check that the (name, kind) is a managed by FreedomBox before
|
||||||
# performing operation.
|
# performing operation.
|
||||||
@privileged
|
@privileged
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
"""
|
"""URLs for the Apache module."""
|
||||||
URLs for the Apache module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
urlpatterns: list = []
|
from django.urls import re_path
|
||||||
|
from stronghold.decorators import public
|
||||||
|
|
||||||
|
from .views import DiscoverIDPView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r'^apache/discover-idp/$', public(DiscoverIDPView.as_view()),
|
||||||
|
name='discover-idp'),
|
||||||
|
]
|
||||||
|
|||||||
71
plinth/modules/apache/views.py
Normal file
71
plinth/modules/apache/views.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
"""Views for the Apache app."""
|
||||||
|
|
||||||
|
from urllib.parse import urlencode, urlparse
|
||||||
|
|
||||||
|
from django.http import (HttpResponseBadRequest, HttpResponseRedirect,
|
||||||
|
HttpResponseServerError)
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from . import setup_oidc_client, validate_host
|
||||||
|
|
||||||
|
# By default 'openid' scope already included by mod_auth_openidc
|
||||||
|
OIDC_SCOPES = 'email freedombox_groups'
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoverIDPView(View):
|
||||||
|
"""A view called by auth_openidc Apache module to find the IDP.
|
||||||
|
|
||||||
|
According to documentation for auth_openidc: an Issuer selection can be
|
||||||
|
passed back to the callback URL as in:
|
||||||
|
<callback-url>?iss=[${issuer}|${domain}|${e-mail-style-account-name}]
|
||||||
|
[parameters][&login_hint=<login-hint>][&scopes=<scopes>]
|
||||||
|
[&auth_request_params=<params>]
|
||||||
|
|
||||||
|
where the <iss> parameter contains the URL-encoded issuer value of the
|
||||||
|
selected Provider (or...), [parameters] contains the additional parameters
|
||||||
|
that were passed in on the discovery request (e.g.
|
||||||
|
target_link_uri=<url>&x_csrf=<x_csrf>&method=<method>&scopes=<scopes>)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""Redirect back to auth_openidc module after selecting a IDP."""
|
||||||
|
target_link_uri = request.GET.get('target_link_uri', '')
|
||||||
|
method = request.GET.get('method', 'get')
|
||||||
|
x_csrf = request.GET.get('x_csrf', '')
|
||||||
|
oidc_callback = request.GET.get('oidc_callback')
|
||||||
|
|
||||||
|
if method != 'get':
|
||||||
|
return HttpResponseBadRequest(f'Cannot handle "{method}" method')
|
||||||
|
|
||||||
|
oidc_callback_parts = urlparse(oidc_callback)
|
||||||
|
request_host = request.META['HTTP_HOST']
|
||||||
|
if request_host != oidc_callback_parts.netloc:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
f'Cannot redirect from {request_host} to a different host '
|
||||||
|
f'{oidc_callback_parts.netloc}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_host(oidc_callback_parts.hostname)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
f'Accessed using unknown domain {request_host}. Please add '
|
||||||
|
'the domain to list of configured domains.')
|
||||||
|
|
||||||
|
try:
|
||||||
|
setup_oidc_client(oidc_callback_parts.netloc,
|
||||||
|
oidc_callback_parts.hostname)
|
||||||
|
except ValueError:
|
||||||
|
return HttpResponseServerError(
|
||||||
|
f'Server not configured to called as {request_host}')
|
||||||
|
|
||||||
|
url = '/apache/oidc/callback'
|
||||||
|
params = {
|
||||||
|
'iss': f'https://{request_host}/freedombox/o',
|
||||||
|
'target_link_uri': target_link_uri,
|
||||||
|
'method': method,
|
||||||
|
'x_csrf': x_csrf,
|
||||||
|
'scopes': OIDC_SCOPES,
|
||||||
|
}
|
||||||
|
params = urlencode(params)
|
||||||
|
return HttpResponseRedirect(f'{url}?{params}')
|
||||||
@ -239,12 +239,12 @@ def is_available(browser, site_name):
|
|||||||
browser.visit(url_to_visit)
|
browser.visit(url_to_visit)
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
browser.reload()
|
browser.reload()
|
||||||
if '404' in browser.title or 'Page not found' in browser.title:
|
if ('404' in browser.title or '401 Unauthorized' in browser.title
|
||||||
|
or 'Page not found' in browser.title):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# The site might have a default path after the sitename,
|
# The site might have a default path after the sitename,
|
||||||
# e.g /mediawiki/Main_Page
|
# e.g /mediawiki/Main_Page
|
||||||
print('URL =', browser.url, url_to_visit, browser.title)
|
|
||||||
browser_url = browser.url.partition('://')[2]
|
browser_url = browser.url.partition('://')[2]
|
||||||
url_to_visit_without_proto = url_to_visit.strip('/').partition('://')[2]
|
url_to_visit_without_proto = url_to_visit.strip('/').partition('://')[2]
|
||||||
return browser_url.startswith(url_to_visit_without_proto) # not a redirect
|
return browser_url.startswith(url_to_visit_without_proto) # not a redirect
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user