Skip to content

Commit ffb5019

Browse files
authored
Auth@Edge User Pool Creation (#194)
1 parent db7aaed commit ffb5019

File tree

8 files changed

+194
-47
lines changed

8 files changed

+194
-47
lines changed

docs/source/module_configuration/staticsite.rst

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,17 @@ Parameters
7878
7979
staticsite_auth_at_edge: true
8080
81+
**staticsite_create_user_pool (Optional[bool])**
82+
Creates a User Pool for the Auth@Edge configuration. Either this or ``staticsite_user_pool_arn`` are required when ``staticsite_auth_at_edge`` is ``true``.
83+
84+
Example:
85+
86+
.. code-block:: yaml
87+
88+
staticsite_create_user_pool: true
89+
8190
**staticsite_user_pool_arn (Optional[str])**
82-
Required if ``staticsite_auth_at_edge`` is ``true``. A pre-existing Cognito User Pool is required for user authentication.
91+
A pre-existing Cognito User Pool is required for user authentication. Either this or ``staticsite_create_user_pool`` are required when ``staticsite_auth_at_edge`` is ``true``.
8392

8493
Example:
8594

@@ -343,7 +352,7 @@ Here is how the solution works:
343352
6. After passing all of the verification steps, `Lambda@Edge` strips out the Authorization header and allows the request to pass through to designated origin for CloudFront. In this case, the origin is the private content Amazon S3 bucket.
344353
7. After receiving response from the origin S3 bucket, CloudFront sends the response back to the browser. The browser displays the data from the returned response.
345354

346-
An example of a `Auth@Edge`_ static site configuration is as follows. All listed options are required:
355+
An example of a `Auth@Edge`_ static site configuration is as follows:
347356

348357
.. code-block:: yaml
349358
@@ -362,5 +371,5 @@ An example of a `Auth@Edge`_ static site configuration is as follows. All listed
362371
- us-east-1
363372
364373
The `Auth@Edge`_ functionality uses your existing Cognito User Pool (optionally configured
365-
with federated identity providers). A user pool app client will be automatically created
366-
within the pool for the application's use.
374+
with federated identity providers) or can create one for you with the ``staticsite_create_user_pool`` option.
375+
A user pool app client will be automatically created within the pool for the application's use.

runway/blueprints/staticsite/dependencies.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ class Dependencies(Blueprint):
2222
'default': False,
2323
'description': 'Utilizing Authorization @ Edge'
2424
},
25+
'CreateUserPool': {
26+
'type': bool,
27+
'default': False,
28+
'description': 'Whether a User Pool should be created for the project'
29+
},
2530
'UserPoolId': {
2631
'type': str,
2732
'default': '',
@@ -113,12 +118,27 @@ def create_template(self):
113118
if variables['AuthAtEdge']:
114119
callbacks = self.context.hook_data['aae_callback_url_retriever']['callback_urls']
115120

121+
user_pool_id = variables['UserPoolId']
122+
123+
if variables['CreateUserPool']:
124+
user_pool = template.add_resource(
125+
cognito.UserPool("AuthAtEdgeUserPool")
126+
)
127+
128+
user_pool_id = user_pool.ref()
129+
130+
template.add_output(Output(
131+
'AuthAtEdgeUserPoolId',
132+
Description='Cognito User Pool App Client for Auth @ Edge',
133+
Value=user_pool_id
134+
))
135+
116136
client = template.add_resource(
117137
cognito.UserPoolClient(
118138
"AuthAtEdgeClient",
119139
AllowedOAuthFlows=['code'],
120140
CallbackURLs=callbacks,
121-
UserPoolId=variables['UserPoolId'],
141+
UserPoolId=user_pool_id,
122142
AllowedOAuthScopes=variables['OAuthScopes']
123143
)
124144
)

runway/hooks/staticsite/auth_at_edge/callback_url_retriever.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,19 @@ def get(context, # pylint: disable=unused-argument
5252
)
5353
# Get the client_id from the outputs
5454
outputs = stack_desc['Stacks'][0]['Outputs']
55+
56+
if kwargs['user_pool_arn']:
57+
user_pool_id = kwargs['user_pool_arn'].split('/')[-1:][0]
58+
else:
59+
user_pool_id = [
60+
o['OutputValue'] for o in outputs if o['OutputKey'] == 'AuthAtEdgeUserPoolId'
61+
][0]
62+
5563
client_id = [o['OutputValue'] for o in outputs if o['OutputKey'] == 'AuthAtEdgeClient'][0]
5664

5765
# Poll the user pool client information
5866
resp = cognito_client.describe_user_pool_client(
59-
UserPoolId=kwargs['user_pool_id'],
67+
UserPoolId=user_pool_id,
6068
ClientId=client_id
6169
)
6270

runway/hooks/staticsite/auth_at_edge/client_updater.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def update(context, # pylint: disable=unused-argument
6363
ClientId=kwargs['client_id'],
6464
CallbackURLs=redirect_uris_sign_in,
6565
LogoutURLs=redirect_uris_sign_out,
66-
UserPoolId=kwargs['user_pool_id'],
66+
UserPoolId=context.hook_data['aae_user_pool_id_retriever']['id'],
6767
)
6868
return True
6969
except Exception as err: # pylint: disable=broad-except

runway/hooks/staticsite/auth_at_edge/domain_updater.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
import logging
33
from typing import Any, Dict, Optional, Union # pylint: disable=unused-import
44

5-
from runway.cfngin.session_cache import get_session
6-
from runway.cfngin.providers.base import BaseProvider # pylint: disable=unused-import
75
from runway.cfngin.context import Context # pylint: disable=unused-import
6+
from runway.cfngin.providers.base import BaseProvider # pylint: disable=unused-import
7+
from runway.cfngin.session_cache import get_session
88

99
LOGGER = logging.getLogger(__name__)
1010

1111

12-
# pylint: disable=unused-argument
1312
def update(context, # type: Context # pylint: disable=unused-import
1413
provider, # type: BaseProvider
1514
**kwargs # type: Optional[Dict[str, Any]]
@@ -36,7 +35,7 @@ def update(context, # type: Context # pylint: disable=unused-import
3635

3736
context_dict = {}
3837

39-
user_pool_id = kwargs['user_pool_id']
38+
user_pool_id = context.hook_data['aae_user_pool_id_retriever']['id']
4039
client_id = kwargs['client_id']
4140
user_pool = cognito_client.describe_user_pool(UserPoolId=user_pool_id).get('UserPool')
4241
(user_pool_region, user_pool_hash) = user_pool_id.split('_')
@@ -72,6 +71,50 @@ def update(context, # type: Context # pylint: disable=unused-import
7271
return False
7372

7473

74+
def delete(context, # type: Context # pylint: disable=unused-import
75+
provider, # type: BaseProvider
76+
**kwargs # type: Optional[Dict[str, Any]]
77+
): # noqa: E124
78+
# type: (...) -> Union[Dict[str, Any], bool]
79+
"""Delete the domain if the user pool was created by Runway.
80+
81+
If a User Pool was created by Runway, and populated with a domain, that
82+
domain must be deleted prior to the User Pool itself being deleted or an
83+
error will occur. This process ensures that our generated domain name is
84+
deleted, or skips if not able to find one.
85+
86+
Args:
87+
context (:class:`runway.cfngin.context.Context`): The context
88+
instance.
89+
provider (:class:`runway.cfngin.providers.base.BaseProvider`):
90+
The provider instance
91+
92+
Keyword Args:
93+
client_id (str): The ID of the Cognito User Pool Client
94+
"""
95+
session = get_session(provider.region)
96+
cognito_client = session.client('cognito-idp')
97+
98+
user_pool_id = context.hook_data['aae_user_pool_id_retriever']['id']
99+
client_id = kwargs['client_id']
100+
(_, user_pool_hash) = user_pool_id.split('_')
101+
domain_prefix = ('%s-%s' % (user_pool_hash, client_id)).lower()
102+
103+
try:
104+
cognito_client.delete_user_pool_domain(
105+
UserPoolId=user_pool_id,
106+
Domain=domain_prefix
107+
)
108+
return True
109+
except cognito_client.exceptions.InvalidParameterException:
110+
LOGGER.info('No domain found with prefix %s. Skipping deletion.', domain_prefix)
111+
return True
112+
except Exception as err: # pylint: disable=broad-except
113+
LOGGER.error('Could not delete the User Pool Domain.')
114+
LOGGER.error(err)
115+
return False
116+
117+
75118
def get_user_pool_domain(prefix, region):
76119
"""Return a user pool domain name based on the prefix received and region.
77120

runway/hooks/staticsite/auth_at_edge/lambda_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def write(context, # type: context.Context
6767
'redirect_path_auth_refresh': kwargs['redirect_path_refresh'],
6868
'redirect_path_sign_in': kwargs['redirect_path_sign_in'],
6969
'redirect_path_sign_out': kwargs['redirect_path_sign_out'],
70-
'user_pool_id': kwargs['user_pool_id'],
70+
'user_pool_id': context.hook_data['aae_user_pool_id_retriever']['id']
7171
}
7272

7373
# Shared file that contains the method called for configuration data
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Retrieve the ID of the Cognito User Pool."""
2+
import logging
3+
4+
from typing import Any, Dict, Optional # pylint: disable=unused-import
5+
6+
from runway.cfngin.providers.base import BaseProvider # pylint: disable=unused-import
7+
from runway.cfngin.context import Context # noqa pylint: disable=unused-import
8+
9+
LOGGER = logging.getLogger(__name__)
10+
11+
12+
def get(context, # pylint: disable=unused-argument
13+
provider, # pylint: disable=unused-argument
14+
**kwargs
15+
): # noqa: E124
16+
# type: (Context, BaseProvider, Optional[Dict[str, Any]]) -> Dict
17+
"""Retrieve the ID of the Cognito User Pool.
18+
19+
The User Pool can either be supplied via an ARN or by being generated.
20+
If the user has supplied an ARN that utilize that, otherwise retrieve
21+
the generated id. Used in multiple pre_hooks for Auth@Edge.
22+
23+
Args:
24+
context (:class:`runway.cfngin.context.Context`): The context
25+
instance.
26+
provider (:class:`runway.cfngin.providers.base.BaseProvider`):
27+
The provider instance
28+
29+
Keyword Args:
30+
user_pool_arn (str): The ARN of the supplied User pool
31+
created_user_pool_id (str): The ID of the created Cognito User Pool
32+
"""
33+
context_dict = {'id': ''}
34+
35+
# Favor a specific arn over a created one
36+
if kwargs['user_pool_arn']:
37+
context_dict['id'] = kwargs['user_pool_arn'].split('/')[-1:][0]
38+
elif kwargs['created_user_pool_id']:
39+
context_dict['id'] = kwargs['created_user_pool_id']
40+
41+
return context_dict

0 commit comments

Comments
 (0)