Skip to content

Commit

Permalink
Merge pull request #35017 from dimagi/sk/connection-settings-sureadhere
Browse files Browse the repository at this point in the history
Updates to connection settings to support Azure AD B2C
  • Loading branch information
snopoke authored Aug 23, 2024
2 parents a1715c4 + feb6730 commit 6d9e608
Show file tree
Hide file tree
Showing 8 changed files with 77 additions and 7 deletions.
16 changes: 14 additions & 2 deletions corehq/motech/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ def __init__(
token_url: str,
refresh_url: str,
pass_credentials_in_header: bool,
include_client_id: bool,
scope: str,
connection_settings: 'ConnectionSettings',
):
self.base_url = base_url
Expand All @@ -193,6 +195,8 @@ def __init__(
self.token_url = token_url
self.refresh_url = refresh_url
self.pass_credentials_in_header = pass_credentials_in_header
self.include_client_id = include_client_id
self.scope = scope
self.connection_settings = connection_settings

@property
Expand All @@ -216,19 +220,21 @@ def set_last_token(token):
self.last_token = token

if not self.last_token or self.last_token.get('refresh_token') is None:
client = BackendApplicationClient(client_id=self.client_id)
client = BackendApplicationClient(client_id=self.client_id, scope=self.scope)
session = OAuth2Session(client=client)
if self.pass_credentials_in_header:
auth = HTTPBasicAuth(self.client_id, self.client_secret)
self.last_token = session.fetch_token(
token_url=self.token_url,
auth=auth,
include_client_id=self.include_client_id,
)
else:
self.last_token = session.fetch_token(
token_url=self.token_url,
client_id=self.client_id,
client_secret=self.client_secret,
include_client_id=self.include_client_id,
)

refresh_kwargs = {
Expand Down Expand Up @@ -266,6 +272,8 @@ def __init__(
token_url: str,
refresh_url: str,
pass_credentials_in_header: bool,
include_client_id: bool,
scope: str,
connection_settings: 'ConnectionSettings',
):
self.base_url = base_url
Expand All @@ -276,6 +284,8 @@ def __init__(
self.token_url = token_url
self.refresh_url = refresh_url
self.pass_credentials_in_header = pass_credentials_in_header
self.include_client_id = include_client_id
self.scope = scope
self.connection_settings = connection_settings

@property
Expand Down Expand Up @@ -303,7 +313,7 @@ def set_last_token(token):
# without error, or refactoring the way sessions are used across
# all repeaters.
if not self.last_token or self.last_token.get('refresh_token') is None:
client = LegacyApplicationClient(client_id=self.client_id)
client = LegacyApplicationClient(client_id=self.client_id, scope=self.scope)
session = OAuth2Session(client=client)
if self.pass_credentials_in_header:
auth = HTTPBasicAuth(self.client_id, self.client_secret)
Expand All @@ -312,6 +322,7 @@ def set_last_token(token):
username=self.username,
password=self.password,
auth=auth,
include_client_id=self.include_client_id,
)
else:
self.last_token = session.fetch_token(
Expand All @@ -320,6 +331,7 @@ def set_last_token(token):
password=self.password,
client_id=self.client_id,
client_secret=self.client_secret,
include_client_id=self.include_client_id,
)

# Return session that refreshes token automatically
Expand Down
19 changes: 17 additions & 2 deletions corehq/motech/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ class ConnectionSettingsForm(forms.ModelForm):
help_text=_('Pass credentials in Basic Auth header when requesting a token'),
required=False,
)
include_client_id = forms.BooleanField(
label=_('Include client ID'),
help_text=_('Send the client ID in the body of the token request'),
required=False,
)
scope = forms.CharField(
label=_('Scope'),
help_text=_('Space-separated list of scopes e.g. "read write"'),
required=False,
)
skip_cert_verify = forms.BooleanField(
label="",
help_text=_('Do not use in a production environment'),
Expand Down Expand Up @@ -92,6 +102,8 @@ class Meta:
'plaintext_password',
'client_id',
'plaintext_client_secret',
'include_client_id',
'scope',
'skip_cert_verify',
'notify_addresses_str',
'token_url',
Expand Down Expand Up @@ -132,6 +144,7 @@ def helper(self):
from corehq.motech.views import ConnectionSettingsListView

helper = hqcrispy.HQFormHelper()
helper.form_class = "form-horizontal"
helper.layout = crispy.Layout(
crispy.Field('name'),
crispy.Field('notify_addresses_str'),
Expand All @@ -147,11 +160,13 @@ def helper(self):
crispy.Field('auth_preset'),
crispy.Field('token_url'),
crispy.Field('refresh_url'),
twbscrispy.PrependedText('pass_credentials_in_header', ''),
crispy.Field('pass_credentials_in_header'),
crispy.Field('include_client_id'),
crispy.Field('scope'),
),
id="div_id_oauth_settings",
),
twbscrispy.PrependedText('skip_cert_verify', ''),
crispy.Field('skip_cert_verify'),
self.test_connection_button,

twbscrispy.StrictButton(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 4.2.14 on 2024-08-22 12:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('motech', '0015_requestlog_duration'),
]

operations = [
migrations.AddField(
model_name='connectionsettings',
name='include_client_id',
field=models.BooleanField(default=None, null=True),
),
migrations.AddField(
model_name='connectionsettings',
name='scope',
field=models.TextField(blank=True, null=True),
),
]
6 changes: 6 additions & 0 deletions corehq/motech/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ class ConnectionSettings(models.Model):
token_url = models.CharField(max_length=255, blank=True, null=True)
refresh_url = models.CharField(max_length=255, blank=True, null=True)
pass_credentials_in_header = models.BooleanField(default=None, null=True)
include_client_id = models.BooleanField(default=None, null=True)
scope = models.TextField(null=True, blank=True)
notify_addresses_str = models.CharField(max_length=255, default="")
# last_token is stored encrypted because it can contain secrets
last_token_aes = models.TextField(blank=True, default="")
Expand Down Expand Up @@ -204,6 +206,8 @@ def get_auth_manager(self):
token_url=self.token_url,
refresh_url=self.refresh_url,
pass_credentials_in_header=self.pass_credentials_in_header,
include_client_id=self.include_client_id,
scope=self.scope or None,
connection_settings=self,
)

Expand Down Expand Up @@ -237,6 +241,8 @@ def get_auth_manager(self):
token_url=self.token_url,
refresh_url=self.refresh_url,
pass_credentials_in_header=self.pass_credentials_in_header,
include_client_id=self.include_client_id,
scope=self.scope or None,
connection_settings=self,
)
raise ValueError(f'Unknown auth type {self.auth_type!r}')
Expand Down
7 changes: 7 additions & 0 deletions corehq/motech/static/motech/js/connection_settings_detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ hqDefine("motech/js/connection_settings_detail", [
'token_url',
'refresh_url',
'pass_credentials_in_header',
'include_client_id',
'scope',
];
if (authPreset === 'CUSTOM') {
_.each(customAuthPresetFields, function (field) {
Expand Down Expand Up @@ -141,6 +143,11 @@ hqDefine("motech/js/connection_settings_detail", [
plaintext_password: $('#id_plaintext_password').val(),
client_id: $('#id_client_id').val(),
plaintext_client_secret: $('#id_plaintext_client_secret').val(),
pass_credentials_in_header: $('#id_pass_credentials_in_header').prop('checked'),
include_client_id: $('#id_include_client_id').prop('checked'),
scope: $('#id_scope').val(),
token_url: $('#id_token_url').val(),
auth_preset: $('#id_auth_preset').val(),
skip_cert_verify: $('#id_skip_cert_verify').prop('checked'),
};
$testConnectionButton.disableButton();
Expand Down
1 change: 0 additions & 1 deletion corehq/motech/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ def split_url(url):
return url.split(connection.url.rstrip('/'))[1]
except IndexError:
return None

for preset_slug, preset in AUTH_PRESETS.items():
if (
split_url(connection.token_url) == preset.token_endpoint
Expand Down
11 changes: 9 additions & 2 deletions corehq/motech/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from corehq.apps.hqwebapp.views import CRUDPaginatedViewMixin
from corehq.apps.users.decorators import require_permission
from corehq.apps.users.models import HqPermissions
from corehq.motech.const import PASSWORD_PLACEHOLDER
from corehq.motech.const import PASSWORD_PLACEHOLDER, OAUTH2_CLIENT, OAUTH2_PWD
from corehq.motech.forms import ConnectionSettingsForm, UnrecognizedHost
from corehq.motech.models import ConnectionSettings, RequestLog
from corehq.util.urlvalidate.urlvalidate import PossibleSSRFAttempt
Expand Down Expand Up @@ -308,7 +308,14 @@ def test_connection_settings(request, domain):
raise Http404

# If auth_type is set to None, we ignore this check
if request.POST.get('plaintext_password') == PASSWORD_PLACEHOLDER and request.POST.get('auth_type'):
auth_type = request.POST.get('auth_type')
client_secret = request.POST.get('plaintext_client_secret')
if auth_type in (OAUTH2_CLIENT, OAUTH2_PWD) and client_secret == PASSWORD_PLACEHOLDER:
return JsonResponse({
"success": False,
"response": _("Please enter client secret again."),
})
if auth_type and request.POST.get('plaintext_password') == PASSWORD_PLACEHOLDER:
# The user is editing an existing instance, and the form is
# showing the password placeholder. (We don't tell the user what
# the API password is.)
Expand Down
1 change: 1 addition & 0 deletions migrations.lock
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@ motech
0013_alter_connectionsettings_auth_type
0014_alter_connectionsettings_password
0015_requestlog_duration
0016_connectionsettings_include_client_id_and_more
notifications
0001_squashed_0003_auto_20160504_2049 (3 squashed migrations)
0002_auto_20160505_2058
Expand Down

0 comments on commit 6d9e608

Please sign in to comment.