Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions backend/common-cdk/common_constructs/user_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def __init__( # pylint: disable=too-many-arguments
)

self.security_profile = security_profile
self.environment_name = environment_name

# Configure notification emails if provided
self.notification_from_email = notification_from_email
Expand Down Expand Up @@ -256,6 +257,7 @@ def add_ui_client(
read_attributes: ClientAttributes,
write_attributes: ClientAttributes,
ui_scopes: list[OAuthScope] = None,
callback_path: str = '/auth/callback',
):
"""
Creates an app client for the UI to authenticate with the user pool.
Expand All @@ -265,15 +267,31 @@ def add_ui_client(
:param read_attributes: The attributes that the UI can read.
:param write_attributes: The attributes that the UI can write.
:param ui_scopes: OAuth scopes that are allowed with this client
:param callback_path: The OAuth redirect path the UI uses for this client. Each user scope/compact has its own
callback page (e.g. '/auth/callback/staff/jcc'), so this must match the redirect_uri the UI sends.
"""
# localhost redirects are only ever appropriate for non-production environments. Fail loudly if a production
# environment is misconfigured (e.g. an SSM parameter accidentally set to 'allow_local_ui: true') rather than
# silently registering a localhost URL on the production app client.
if self.environment_name == 'prod' and environment_context.get('allow_local_ui', False):
raise ValueError("'allow_local_ui' must not be enabled in the production environment")
# Defensive fallback: even if the guard above is ever refactored, never allow localhost redirects in production.
allow_local_ui = environment_context.get('allow_local_ui', False) and self.environment_name != 'prod'

callback_urls = []
if ui_domain_name is not None:
callback_urls.append(f'https://{ui_domain_name}/auth/callback')
callback_urls.append(f'https://{ui_domain_name}{callback_path}')
Comment thread
jsandoval81 marked this conversation as resolved.
# TODO - remove after cutover to custom callback paths is deployed #noqa: FIX002
if callback_path != '/auth/callback':
callback_urls.append(f'https://{ui_domain_name}/auth/callback')
# This toggle will allow front-end devs to point their local UI at this environment's user pool to support
# authenticated actions.
if environment_context.get('allow_local_ui', False):
if allow_local_ui:
local_ui_port = environment_context.get('local_ui_port', '3018')
callback_urls.append(f'http://localhost:{local_ui_port}/auth/callback')
callback_urls.append(f'http://localhost:{local_ui_port}{callback_path}')
# TODO - remove after cutover to custom callback paths is deployed #noqa: FIX002
if callback_path != '/auth/callback':
callback_urls.append(f'http://localhost:{local_ui_port}/auth/callback')
if not callback_urls:
raise ValueError(
"This app requires a callback url for its authentication path. Either provide 'domain_name' or set "
Expand All @@ -287,7 +305,7 @@ def add_ui_client(
logout_urls.append(f'https://{ui_domain_name}/Logout')
# This toggle will allow front-end devs to point their local UI at this environment's user pool to support
# authenticated actions.
if environment_context.get('allow_local_ui', False):
if allow_local_ui:
local_ui_port = environment_context.get('local_ui_port', '3018')
logout_urls.append(f'http://localhost:{local_ui_port}/Login')
logout_urls.append(f'http://localhost:{local_ui_port}/Dashboard')
Expand Down
79 changes: 79 additions & 0 deletions backend/common-cdk/tests/test_user_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,85 @@ def test_ui_client_requires_callback_url(self):
write_attributes=None,
)

def test_ui_client_uses_default_callback_path(self):
pool = _make_pool(self.stack)
pool.add_ui_client(
ui_domain_name='app.example.com',
environment_context={'allow_local_ui': True, 'local_ui_port': '3000'},
read_attributes=None,
write_attributes=None,
)

template = Template.from_stack(self.stack)
template.has_resource_properties(
CfnUserPoolClient.CFN_RESOURCE_TYPE_NAME,
{
'CallbackURLs': [
'https://app.example.com/auth/callback',
'http://localhost:3000/auth/callback',
],
},
)

def test_ui_client_uses_custom_callback_path(self):
pool = _make_pool(self.stack)
pool.add_ui_client(
ui_domain_name='app.example.com',
environment_context={'allow_local_ui': True, 'local_ui_port': '3000'},
read_attributes=None,
write_attributes=None,
callback_path='/auth/callback/staff/jcc',
)

template = Template.from_stack(self.stack)
template.has_resource_properties(
CfnUserPoolClient.CFN_RESOURCE_TYPE_NAME,
{
'CallbackURLs': [
'https://app.example.com/auth/callback/staff/jcc',
'https://app.example.com/auth/callback',
'http://localhost:3000/auth/callback/staff/jcc',
'http://localhost:3000/auth/callback',
],
},
)

def test_ui_client_raises_when_local_ui_allowed_in_prod(self):
# A production environment must never enable localhost redirects. If an SSM parameter is accidentally set to
# 'allow_local_ui: true' for prod, synthesis should fail loudly rather than register a localhost callback.
pool = _make_pool(self.stack, environment_name='prod')
with self.assertRaisesRegex(ValueError, 'allow_local_ui'):
pool.add_ui_client(
ui_domain_name='app.example.com',
environment_context={'allow_local_ui': True, 'local_ui_port': '3000'},
read_attributes=None,
write_attributes=None,
callback_path='/auth/callback/staff/jcc',
)

def test_ui_client_prod_excludes_localhost(self):
# Even when the guard is not tripped (flag absent), prod must only ever register its hosted-domain redirects,
# never localhost.
pool = _make_pool(self.stack, environment_name='prod')
pool.add_ui_client(
ui_domain_name='app.example.com',
environment_context={},
read_attributes=None,
write_attributes=None,
callback_path='/auth/callback/staff/jcc',
)

template = Template.from_stack(self.stack)
template.has_resource_properties(
CfnUserPoolClient.CFN_RESOURCE_TYPE_NAME,
{
'CallbackURLs': [
'https://app.example.com/auth/callback/staff/jcc',
'https://app.example.com/auth/callback',
],
},
)

def test_add_default_app_client_domain_creates_cognito_domain(self):
pool = _make_pool(self.stack)
pool.add_default_app_client_domain('testprefix')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/jcc"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/token?grant_type=authorization_code&code=f23723c3-1d21-40e1-89ec-64807d2d658d&client_id={{clientId}}&scope=openid&redirect_uri=http://localhost:3018/auth/callback"
"raw": "{{staffUserPoolUrl}}/oauth2/token?grant_type=authorization_code&code=f23723c3-1d21-40e1-89ec-64807d2d658d&client_id={{clientId}}&scope=openid&redirect_uri=http://localhost:3018/auth/callback/staff/jcc"
}
},
"response": []
Expand Down Expand Up @@ -237,14 +237,14 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/jcc"
},
{
"key": "scope",
"value": "openid"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback&scope=openid"
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback/staff/jcc&scope=openid"
}
},
"response": []
Expand Down Expand Up @@ -309,14 +309,14 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/jcc"
},
{
"key": "identity_provider",
"value": "COGNITO"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?scope=openid&response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback&identity_provider=COGNITO"
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?scope=openid&response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback/staff/jcc&identity_provider=COGNITO"
}
},
"response": []
Expand Down
12 changes: 6 additions & 6 deletions backend/compact-connect/docs/postman/postman-collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/jcc"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/token?grant_type=authorization_code&code=f23723c3-1d21-40e1-89ec-64807d2d658d&client_id={{clientId}}&scope=openid&redirect_uri=http://localhost:3018/auth/callback"
"raw": "{{staffUserPoolUrl}}/oauth2/token?grant_type=authorization_code&code=f23723c3-1d21-40e1-89ec-64807d2d658d&client_id={{clientId}}&scope=openid&redirect_uri=http://localhost:3018/auth/callback/staff/jcc"
}
},
"response": []
Expand Down Expand Up @@ -237,14 +237,14 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/jcc"
},
{
"key": "scope",
"value": "openid"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback&scope=openid"
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback/staff/jcc&scope=openid"
}
},
"response": []
Expand Down Expand Up @@ -309,14 +309,14 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/jcc"
},
{
"key": "identity_provider",
"value": "COGNITO"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?scope=openid&response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback&identity_provider=COGNITO"
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?scope=openid&response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback/staff/jcc&identity_provider=COGNITO"
}
},
"response": []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ def __init__(
self.ui_client = self.add_ui_client(
ui_domain_name=stack.ui_domain_name,
environment_context=environment_context,
callback_path='/auth/callback/staff/jcc',
# We have to provide one True value or CFn will make every attribute writeable
write_attributes=ClientAttributes().with_standard_attributes(email=True),
# We want to limit the attributes that this app can read and write so only email is visible.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def __init__(
self.ui_client = self.add_ui_client(
ui_domain_name=persistent_stack.ui_domain_name,
environment_context=environment_context,
callback_path='/auth/callback/licensee/jcc',
# For now, we are allowing the user to read and update their email.
# we only allow the user to be able to see their providerId and compact, which are custom attributes.
# If we ever want other attributes to be read or written, they must be added here.
Expand Down
4 changes: 4 additions & 0 deletions backend/compact-connect/tests/app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,12 @@ def _inspect_provider_users_stack(
provider_users_stack_template = Template.from_stack(provider_users_stack)
callbacks = []
if domain_name is not None:
callbacks.append(f'https://{domain_name}/auth/callback/licensee/jcc')
callbacks.append(f'https://{domain_name}/auth/callback')
if allow_local_ui:
# 3018 is default
local_ui_port = '3018' if not local_ui_port else local_ui_port
callbacks.append(f'http://localhost:{local_ui_port}/auth/callback/licensee/jcc')
callbacks.append(f'http://localhost:{local_ui_port}/auth/callback')

# Ensure our provider user pool is created with expected custom attributes
Expand Down Expand Up @@ -209,10 +211,12 @@ def _inspect_persistent_stack(

callbacks = []
if domain_name is not None:
callbacks.append(f'https://{domain_name}/auth/callback/staff/jcc')
callbacks.append(f'https://{domain_name}/auth/callback')
if allow_local_ui:
# 3018 is default
local_ui_port = '3018' if not local_ui_port else local_ui_port
callbacks.append(f'http://localhost:{local_ui_port}/auth/callback/staff/jcc')
callbacks.append(f'http://localhost:{local_ui_port}/auth/callback')

# ensure we have one user pool defined in persistent stack for staff users (provider user pool defined in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,10 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/cosmo"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/token?grant_type=authorization_code&code=f23723c3-1d21-40e1-89ec-64807d2d658d&client_id={{clientId}}&scope=openid&redirect_uri=http://localhost:3018/auth/callback"
"raw": "{{staffUserPoolUrl}}/oauth2/token?grant_type=authorization_code&code=f23723c3-1d21-40e1-89ec-64807d2d658d&client_id={{clientId}}&scope=openid&redirect_uri=http://localhost:3018/auth/callback/staff/cosmo"
}
},
"response": []
Expand Down Expand Up @@ -237,14 +237,14 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/cosmo"
},
{
"key": "scope",
"value": "openid"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback&scope=openid"
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback/staff/cosmo&scope=openid"
}
},
"response": []
Expand Down Expand Up @@ -309,14 +309,14 @@
},
{
"key": "redirect_uri",
"value": "http://localhost:3018/auth/callback"
"value": "http://localhost:3018/auth/callback/staff/cosmo"
},
{
"key": "identity_provider",
"value": "COGNITO"
}
],
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?scope=openid&response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback&identity_provider=COGNITO"
"raw": "{{staffUserPoolUrl}}/oauth2/authorize?scope=openid&response_type=code&client_id={{clientId}}&redirect_uri=http://localhost:3018/auth/callback/staff/cosmo&identity_provider=COGNITO"
}
},
"response": []
Expand Down
Loading
Loading