From e111cd1ff92796651d877e0163da5506e292873f Mon Sep 17 00:00:00 2001 From: jbergknoff-10e Date: Wed, 2 May 2018 16:13:12 -0500 Subject: [PATCH] Implement some of cognito-idp --- MANIFEST.in | 3 +- README.md | 2 + moto/__init__.py | 1 + moto/backends.py | 2 + moto/cognitoidp/__init__.py | 6 + moto/cognitoidp/exceptions.py | 34 ++ moto/cognitoidp/models.py | 512 +++++++++++++++++++ moto/cognitoidp/resources/jwks-private.json | 9 + moto/cognitoidp/resources/jwks-public.json | 12 + moto/cognitoidp/responses.py | 235 +++++++++ moto/cognitoidp/urls.py | 11 + moto/server.py | 7 +- setup.py | 1 + tests/test_cognitoidp/test_cognitoidp.py | 539 ++++++++++++++++++++ 14 files changed, 1372 insertions(+), 2 deletions(-) create mode 100644 moto/cognitoidp/__init__.py create mode 100644 moto/cognitoidp/exceptions.py create mode 100644 moto/cognitoidp/models.py create mode 100644 moto/cognitoidp/resources/jwks-private.json create mode 100644 moto/cognitoidp/resources/jwks-public.json create mode 100644 moto/cognitoidp/responses.py create mode 100644 moto/cognitoidp/urls.py create mode 100644 tests/test_cognitoidp/test_cognitoidp.py diff --git a/MANIFEST.in b/MANIFEST.in index 43e8120e4..bd7eb968a 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,6 @@ include README.md LICENSE AUTHORS.md include requirements.txt requirements-dev.txt tox.ini include moto/ec2/resources/instance_types.json include moto/ec2/resources/amis.json +include moto/cognitoidp/resources/*.json recursive-include moto/templates * -recursive-include tests * +recursive-include tests * diff --git a/README.md b/README.md index 9642a8db6..ff69fc171 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,8 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L |------------------------------------------------------------------------------| | Cognito Identity | @mock_cognitoidentity| basic endpoints done | |------------------------------------------------------------------------------| +| Cognito Identity Provider | @mock_cognitoidp| basic endpoints done | +|------------------------------------------------------------------------------| | Data Pipeline | @mock_datapipeline| basic endpoints done | |------------------------------------------------------------------------------| | DynamoDB | @mock_dynamodb | core endpoints done | diff --git a/moto/__init__.py b/moto/__init__.py index c6f24388b..5e6f71b7a 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -12,6 +12,7 @@ from .awslambda import mock_lambda, mock_lambda_deprecated # flake8: noqa from .cloudformation import mock_cloudformation, mock_cloudformation_deprecated # flake8: noqa from .cloudwatch import mock_cloudwatch, mock_cloudwatch_deprecated # flake8: noqa from .cognitoidentity import mock_cognitoidentity, mock_cognitoidentity_deprecated # flake8: noqa +from .cognitoidp import mock_cognitoidp, mock_cognitoidp_deprecated # flake8: noqa from .datapipeline import mock_datapipeline, mock_datapipeline_deprecated # flake8: noqa from .dynamodb import mock_dynamodb, mock_dynamodb_deprecated # flake8: noqa from .dynamodb2 import mock_dynamodb2, mock_dynamodb2_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index d8d317573..496af13e1 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -7,6 +7,7 @@ from moto.awslambda import lambda_backends from moto.cloudformation import cloudformation_backends from moto.cloudwatch import cloudwatch_backends from moto.cognitoidentity import cognitoidentity_backends +from moto.cognitoidp import cognitoidp_backends from moto.core import moto_api_backends from moto.datapipeline import datapipeline_backends from moto.dynamodb import dynamodb_backends @@ -51,6 +52,7 @@ BACKENDS = { 'cloudformation': cloudformation_backends, 'cloudwatch': cloudwatch_backends, 'cognito-identity': cognitoidentity_backends, + 'cognito-idp': cognitoidp_backends, 'datapipeline': datapipeline_backends, 'dynamodb': dynamodb_backends, 'dynamodb2': dynamodb_backends2, diff --git a/moto/cognitoidp/__init__.py b/moto/cognitoidp/__init__.py new file mode 100644 index 000000000..676e2dd77 --- /dev/null +++ b/moto/cognitoidp/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import cognitoidp_backends +from ..core.models import base_decorator, deprecated_base_decorator + +mock_cognitoidp = base_decorator(cognitoidp_backends) +mock_cognitoidp_deprecated = deprecated_base_decorator(cognitoidp_backends) diff --git a/moto/cognitoidp/exceptions.py b/moto/cognitoidp/exceptions.py new file mode 100644 index 000000000..1f1ec2309 --- /dev/null +++ b/moto/cognitoidp/exceptions.py @@ -0,0 +1,34 @@ +from __future__ import unicode_literals + +import json +from werkzeug.exceptions import BadRequest + + +class ResourceNotFoundError(BadRequest): + + def __init__(self, message): + super(ResourceNotFoundError, self).__init__() + self.description = json.dumps({ + "message": message, + '__type': 'ResourceNotFoundException', + }) + + +class UserNotFoundError(BadRequest): + + def __init__(self, message): + super(UserNotFoundError, self).__init__() + self.description = json.dumps({ + "message": message, + '__type': 'UserNotFoundException', + }) + + +class NotAuthorizedError(BadRequest): + + def __init__(self, message): + super(NotAuthorizedError, self).__init__() + self.description = json.dumps({ + "message": message, + '__type': 'NotAuthorizedException', + }) diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py new file mode 100644 index 000000000..52a73f89f --- /dev/null +++ b/moto/cognitoidp/models.py @@ -0,0 +1,512 @@ +from __future__ import unicode_literals + +import datetime +import json +import os +import time +import uuid + +import boto.cognito.identity +from jose import jws + +from moto.compat import OrderedDict +from moto.core import BaseBackend, BaseModel +from .exceptions import NotAuthorizedError, ResourceNotFoundError, UserNotFoundError + + +UserStatus = { + "FORCE_CHANGE_PASSWORD": "FORCE_CHANGE_PASSWORD", + "CONFIRMED": "CONFIRMED", +} + + +class CognitoIdpUserPool(BaseModel): + + def __init__(self, region, name, extended_config): + self.region = region + self.id = str(uuid.uuid4()) + self.name = name + self.status = None + self.extended_config = extended_config or {} + self.creation_date = datetime.datetime.utcnow() + self.last_modified_date = datetime.datetime.utcnow() + + self.clients = OrderedDict() + self.identity_providers = OrderedDict() + self.users = OrderedDict() + self.refresh_tokens = {} + self.access_tokens = {} + self.id_tokens = {} + + with open(os.path.join(os.path.dirname(__file__), "resources/jwks-private.json")) as f: + self.json_web_key = json.loads(f.read()) + + def _base_json(self): + return { + "Id": self.id, + "Name": self.name, + "Status": self.status, + "CreationDate": time.mktime(self.creation_date.timetuple()), + "LastModifiedDate": time.mktime(self.last_modified_date.timetuple()), + } + + def to_json(self, extended=False): + user_pool_json = self._base_json() + if extended: + user_pool_json.update(self.extended_config) + else: + user_pool_json["LambdaConfig"] = self.extended_config.get("LambdaConfig") or {} + + return user_pool_json + + def create_jwt(self, client_id, username, expires_in=60 * 60, extra_data={}): + now = int(time.time()) + payload = { + "iss": "https://cognito-idp.{}.amazonaws.com/{}".format(self.region, self.id), + "sub": self.users[username].id, + "aud": client_id, + "token_use": "id", + "auth_time": now, + "exp": now + expires_in, + } + payload.update(extra_data) + + return jws.sign(payload, self.json_web_key, algorithm='RS256'), expires_in + + def create_id_token(self, client_id, username): + id_token, expires_in = self.create_jwt(client_id, username) + self.id_tokens[id_token] = (client_id, username) + return id_token, expires_in + + def create_refresh_token(self, client_id, username): + refresh_token = str(uuid.uuid4()) + self.refresh_tokens[refresh_token] = (client_id, username) + return refresh_token + + def create_access_token(self, client_id, username): + access_token, expires_in = self.create_jwt(client_id, username) + self.access_tokens[access_token] = (client_id, username) + return access_token, expires_in + + def create_tokens_from_refresh_token(self, refresh_token): + client_id, username = self.refresh_tokens.get(refresh_token) + if not username: + raise NotAuthorizedError(refresh_token) + + access_token, expires_in = self.create_access_token(client_id, username) + id_token, _ = self.create_id_token(client_id, username) + return access_token, id_token, expires_in + + +class CognitoIdpUserPoolDomain(BaseModel): + + def __init__(self, user_pool_id, domain): + self.user_pool_id = user_pool_id + self.domain = domain + + def to_json(self): + return { + "UserPoolId": self.user_pool_id, + "AWSAccountId": str(uuid.uuid4()), + "CloudFrontDistribution": None, + "Domain": self.domain, + "S3Bucket": None, + "Status": "ACTIVE", + "Version": None, + } + + +class CognitoIdpUserPoolClient(BaseModel): + + def __init__(self, user_pool_id, extended_config): + self.user_pool_id = user_pool_id + self.id = str(uuid.uuid4()) + self.secret = str(uuid.uuid4()) + self.extended_config = extended_config or {} + + def _base_json(self): + return { + "ClientId": self.id, + "ClientName": self.extended_config.get("ClientName"), + "UserPoolId": self.user_pool_id, + } + + def to_json(self, extended=False): + user_pool_client_json = self._base_json() + if extended: + user_pool_client_json.update(self.extended_config) + + return user_pool_client_json + + +class CognitoIdpIdentityProvider(BaseModel): + + def __init__(self, name, extended_config): + self.name = name + self.extended_config = extended_config or {} + self.creation_date = datetime.datetime.utcnow() + self.last_modified_date = datetime.datetime.utcnow() + + def _base_json(self): + return { + "ProviderName": self.name, + "ProviderType": self.extended_config.get("ProviderType"), + "CreationDate": time.mktime(self.creation_date.timetuple()), + "LastModifiedDate": time.mktime(self.last_modified_date.timetuple()), + } + + def to_json(self, extended=False): + identity_provider_json = self._base_json() + if extended: + identity_provider_json.update(self.extended_config) + + return identity_provider_json + + +class CognitoIdpUser(BaseModel): + + def __init__(self, user_pool_id, username, password, status, attributes): + self.id = str(uuid.uuid4()) + self.user_pool_id = user_pool_id + self.username = username + self.password = password + self.status = status + self.enabled = True + self.attributes = attributes + self.create_date = datetime.datetime.utcnow() + self.last_modified_date = datetime.datetime.utcnow() + + def _base_json(self): + return { + "UserPoolId": self.user_pool_id, + "Username": self.username, + "UserStatus": self.status, + "UserCreateDate": time.mktime(self.create_date.timetuple()), + "UserLastModifiedDate": time.mktime(self.last_modified_date.timetuple()), + } + + # list_users brings back "Attributes" while admin_get_user brings back "UserAttributes". + def to_json(self, extended=False, attributes_key="Attributes"): + user_json = self._base_json() + if extended: + user_json.update( + { + "Enabled": self.enabled, + attributes_key: self.attributes, + "MFAOptions": [] + } + ) + + return user_json + + +class CognitoIdpBackend(BaseBackend): + + def __init__(self, region): + super(CognitoIdpBackend, self).__init__() + self.region = region + self.user_pools = OrderedDict() + self.user_pool_domains = OrderedDict() + self.sessions = {} + + def reset(self): + region = self.region + self.__dict__ = {} + self.__init__(region) + + # User pool + def create_user_pool(self, name, extended_config): + user_pool = CognitoIdpUserPool(self.region, name, extended_config) + self.user_pools[user_pool.id] = user_pool + return user_pool + + def list_user_pools(self): + return self.user_pools.values() + + def describe_user_pool(self, user_pool_id): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + return user_pool + + def delete_user_pool(self, user_pool_id): + if user_pool_id not in self.user_pools: + raise ResourceNotFoundError(user_pool_id) + + del self.user_pools[user_pool_id] + + # User pool domain + def create_user_pool_domain(self, user_pool_id, domain): + if user_pool_id not in self.user_pools: + raise ResourceNotFoundError(user_pool_id) + + user_pool_domain = CognitoIdpUserPoolDomain(user_pool_id, domain) + self.user_pool_domains[domain] = user_pool_domain + return user_pool_domain + + def describe_user_pool_domain(self, domain): + if domain not in self.user_pool_domains: + return None + + return self.user_pool_domains[domain] + + def delete_user_pool_domain(self, domain): + if domain not in self.user_pool_domains: + raise ResourceNotFoundError(domain) + + del self.user_pool_domains[domain] + + # User pool client + def create_user_pool_client(self, user_pool_id, extended_config): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + user_pool_client = CognitoIdpUserPoolClient(user_pool_id, extended_config) + user_pool.clients[user_pool_client.id] = user_pool_client + return user_pool_client + + def list_user_pool_clients(self, user_pool_id): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + return user_pool.clients.values() + + def describe_user_pool_client(self, user_pool_id, client_id): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + client = user_pool.clients.get(client_id) + if not client: + raise ResourceNotFoundError(client_id) + + return client + + def update_user_pool_client(self, user_pool_id, client_id, extended_config): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + client = user_pool.clients.get(client_id) + if not client: + raise ResourceNotFoundError(client_id) + + client.extended_config.update(extended_config) + return client + + def delete_user_pool_client(self, user_pool_id, client_id): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + if client_id not in user_pool.clients: + raise ResourceNotFoundError(client_id) + + del user_pool.clients[client_id] + + # Identity provider + def create_identity_provider(self, user_pool_id, name, extended_config): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + identity_provider = CognitoIdpIdentityProvider(name, extended_config) + user_pool.identity_providers[name] = identity_provider + return identity_provider + + def list_identity_providers(self, user_pool_id): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + return user_pool.identity_providers.values() + + def describe_identity_provider(self, user_pool_id, name): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + identity_provider = user_pool.identity_providers.get(name) + if not identity_provider: + raise ResourceNotFoundError(name) + + return identity_provider + + def delete_identity_provider(self, user_pool_id, name): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + if name not in user_pool.identity_providers: + raise ResourceNotFoundError(name) + + del user_pool.identity_providers[name] + + # User + def admin_create_user(self, user_pool_id, username, temporary_password, attributes): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + user = CognitoIdpUser(user_pool_id, username, temporary_password, UserStatus["FORCE_CHANGE_PASSWORD"], attributes) + user_pool.users[user.username] = user + return user + + def admin_get_user(self, user_pool_id, username): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + if username not in user_pool.users: + raise ResourceNotFoundError(username) + + return user_pool.users[username] + + def list_users(self, user_pool_id): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + return user_pool.users.values() + + def admin_delete_user(self, user_pool_id, username): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + if username not in user_pool.users: + raise ResourceNotFoundError(username) + + del user_pool.users[username] + + def _log_user_in(self, user_pool, client, username): + refresh_token = user_pool.create_refresh_token(client.id, username) + access_token, id_token, expires_in = user_pool.create_tokens_from_refresh_token(refresh_token) + + return { + "AuthenticationResult": { + "IdToken": id_token, + "AccessToken": access_token, + "RefreshToken": refresh_token, + "ExpiresIn": expires_in, + } + } + + def admin_initiate_auth(self, user_pool_id, client_id, auth_flow, auth_parameters): + user_pool = self.user_pools.get(user_pool_id) + if not user_pool: + raise ResourceNotFoundError(user_pool_id) + + client = user_pool.clients.get(client_id) + if not client: + raise ResourceNotFoundError(client_id) + + if auth_flow == "ADMIN_NO_SRP_AUTH": + username = auth_parameters.get("USERNAME") + password = auth_parameters.get("PASSWORD") + user = user_pool.users.get(username) + if not user: + raise UserNotFoundError(username) + + if user.password != password: + raise NotAuthorizedError(username) + + if user.status == UserStatus["FORCE_CHANGE_PASSWORD"]: + session = str(uuid.uuid4()) + self.sessions[session] = user_pool + + return { + "ChallengeName": "NEW_PASSWORD_REQUIRED", + "ChallengeParameters": {}, + "Session": session, + } + + return self._log_user_in(user_pool, client, username) + elif auth_flow == "REFRESH_TOKEN": + refresh_token = auth_parameters.get("REFRESH_TOKEN") + id_token, access_token, expires_in = user_pool.create_tokens_from_refresh_token(refresh_token) + + return { + "AuthenticationResult": { + "IdToken": id_token, + "AccessToken": access_token, + "ExpiresIn": expires_in, + } + } + else: + return {} + + def respond_to_auth_challenge(self, session, client_id, challenge_name, challenge_responses): + user_pool = self.sessions.get(session) + if not user_pool: + raise ResourceNotFoundError(session) + + client = user_pool.clients.get(client_id) + if not client: + raise ResourceNotFoundError(client_id) + + if challenge_name == "NEW_PASSWORD_REQUIRED": + username = challenge_responses.get("USERNAME") + new_password = challenge_responses.get("NEW_PASSWORD") + user = user_pool.users.get(username) + if not user: + raise UserNotFoundError(username) + + user.password = new_password + user.status = UserStatus["CONFIRMED"] + del self.sessions[session] + + return self._log_user_in(user_pool, client, username) + else: + return {} + + def confirm_forgot_password(self, client_id, username, password): + for user_pool in self.user_pools.values(): + if client_id in user_pool.clients and username in user_pool.users: + user_pool.users[username].password = password + break + else: + raise ResourceNotFoundError(client_id) + + def change_password(self, access_token, previous_password, proposed_password): + for user_pool in self.user_pools.values(): + if access_token in user_pool.access_tokens: + _, username = user_pool.access_tokens[access_token] + user = user_pool.users.get(username) + if not user: + raise UserNotFoundError(username) + + if user.password != previous_password: + raise NotAuthorizedError(username) + + user.password = proposed_password + if user.status == UserStatus["FORCE_CHANGE_PASSWORD"]: + user.status = UserStatus["CONFIRMED"] + + break + else: + raise NotAuthorizedError(access_token) + + +cognitoidp_backends = {} +for region in boto.cognito.identity.regions(): + cognitoidp_backends[region.name] = CognitoIdpBackend(region.name) + + +# Hack to help moto-server process requests on localhost, where the region isn't +# specified in the host header. Some endpoints (change password, confirm forgot +# password) have no authorization header from which to extract the region. +def find_region_by_value(key, value): + for region in cognitoidp_backends: + backend = cognitoidp_backends[region] + for user_pool in backend.user_pools.values(): + if key == "client_id" and value in user_pool.clients: + return region + + if key == "access_token" and value in user_pool.access_tokens: + return region + + return cognitoidp_backends.keys()[0] diff --git a/moto/cognitoidp/resources/jwks-private.json b/moto/cognitoidp/resources/jwks-private.json new file mode 100644 index 000000000..8dde9c336 --- /dev/null +++ b/moto/cognitoidp/resources/jwks-private.json @@ -0,0 +1,9 @@ +{ + "alg": "RS256", + "d": "DrrLT2qMERN0Id-bNglOe6SVkUNF3MTIzrH-TVkMZhsHk8kyqiqt-8JbLQMh2gOgTIjpu93b2_UREGA0BGdWs34hv0v7Gx8uIngCY6e6XO8LDemOo-2VHZHl5Ew-lrRYhwq12c_c4mfavAdMzXHODrpXSnqLnbFK88S-3fu6Da4czc4Svo4v8MkGZk_fcTml3Y1jIFHxbbTWka37j4NLpAzdfvX--J086m-LbZ8CJL_lGMKbAKsWURMmzCFL9ZFH9JdzX79KeDOH0GrzGwS_cOsZHsCamF_CWrtG4asPt-SHyn_k0X4JJJgAWVA674VCqorMAPDVYIzKJOUMImmsEQ", + "e": "AQAB", + "kid": "dummy", + "kty": "RSA", + "n": "j1pT3xKbswmMySvCefmiD3mfDaRFpZ9Y3Jl4fF0hMaCRVAt_e0yR7BeueDfqmj_NhVSO0WB5ao5e8V-9RFQOtK8SrqKl3i01-CyWYPICwybaGKhbJJR0S_6cZ8n5kscF1MjpIlsJcCzm-yKgTc3Mxk6KtrLoNgRvMwGLeHUXPkhS9YHfDKRe864iMFOK4df69brIYEICG2VLduh0hXYa0i-J3drwm7vxNdX7pVpCDu34qJtYoWq6CXt3Tzfi3YfWp8cFjGNbaDa3WnCd2IXpp0TFsFS-cEsw5rJjSl5OllJGeZKBtLeyVTy9PYwnk7MW43WSYeYstbk9NluX4H8Iuw", + "use": "sig" +} diff --git a/moto/cognitoidp/resources/jwks-public.json b/moto/cognitoidp/resources/jwks-public.json new file mode 100644 index 000000000..a5309c7f5 --- /dev/null +++ b/moto/cognitoidp/resources/jwks-public.json @@ -0,0 +1,12 @@ +{ + "keys": [ + { + "alg": "RS256", + "e": "AQAB", + "kid": "dummy", + "kty": "RSA", + "n": "j1pT3xKbswmMySvCefmiD3mfDaRFpZ9Y3Jl4fF0hMaCRVAt_e0yR7BeueDfqmj_NhVSO0WB5ao5e8V-9RFQOtK8SrqKl3i01-CyWYPICwybaGKhbJJR0S_6cZ8n5kscF1MjpIlsJcCzm-yKgTc3Mxk6KtrLoNgRvMwGLeHUXPkhS9YHfDKRe864iMFOK4df69brIYEICG2VLduh0hXYa0i-J3drwm7vxNdX7pVpCDu34qJtYoWq6CXt3Tzfi3YfWp8cFjGNbaDa3WnCd2IXpp0TFsFS-cEsw5rJjSl5OllJGeZKBtLeyVTy9PYwnk7MW43WSYeYstbk9NluX4H8Iuw", + "use": "sig" + } + ] +} diff --git a/moto/cognitoidp/responses.py b/moto/cognitoidp/responses.py new file mode 100644 index 000000000..e6f20367e --- /dev/null +++ b/moto/cognitoidp/responses.py @@ -0,0 +1,235 @@ +from __future__ import unicode_literals + +import json +import os + +from moto.core.responses import BaseResponse +from .models import cognitoidp_backends, find_region_by_value + + +class CognitoIdpResponse(BaseResponse): + + @property + def parameters(self): + return json.loads(self.body) + + # User pool + def create_user_pool(self): + name = self.parameters.pop("PoolName") + user_pool = cognitoidp_backends[self.region].create_user_pool(name, self.parameters) + return json.dumps({ + "UserPool": user_pool.to_json(extended=True) + }) + + def list_user_pools(self): + user_pools = cognitoidp_backends[self.region].list_user_pools() + return json.dumps({ + "UserPools": [user_pool.to_json() for user_pool in user_pools] + }) + + def describe_user_pool(self): + user_pool_id = self._get_param("UserPoolId") + user_pool = cognitoidp_backends[self.region].describe_user_pool(user_pool_id) + return json.dumps({ + "UserPool": user_pool.to_json(extended=True) + }) + + def delete_user_pool(self): + user_pool_id = self._get_param("UserPoolId") + cognitoidp_backends[self.region].delete_user_pool(user_pool_id) + return "" + + # User pool domain + def create_user_pool_domain(self): + domain = self._get_param("Domain") + user_pool_id = self._get_param("UserPoolId") + cognitoidp_backends[self.region].create_user_pool_domain(user_pool_id, domain) + return "" + + def describe_user_pool_domain(self): + domain = self._get_param("Domain") + user_pool_domain = cognitoidp_backends[self.region].describe_user_pool_domain(domain) + domain_description = {} + if user_pool_domain: + domain_description = user_pool_domain.to_json() + + return json.dumps({ + "DomainDescription": domain_description + }) + + def delete_user_pool_domain(self): + domain = self._get_param("Domain") + cognitoidp_backends[self.region].delete_user_pool_domain(domain) + return "" + + # User pool client + def create_user_pool_client(self): + user_pool_id = self.parameters.pop("UserPoolId") + user_pool_client = cognitoidp_backends[self.region].create_user_pool_client(user_pool_id, self.parameters) + return json.dumps({ + "UserPoolClient": user_pool_client.to_json(extended=True) + }) + + def list_user_pool_clients(self): + user_pool_id = self._get_param("UserPoolId") + user_pool_clients = cognitoidp_backends[self.region].list_user_pool_clients(user_pool_id) + return json.dumps({ + "UserPoolClients": [user_pool_client.to_json() for user_pool_client in user_pool_clients] + }) + + def describe_user_pool_client(self): + user_pool_id = self._get_param("UserPoolId") + client_id = self._get_param("ClientId") + user_pool_client = cognitoidp_backends[self.region].describe_user_pool_client(user_pool_id, client_id) + return json.dumps({ + "UserPoolClient": user_pool_client.to_json(extended=True) + }) + + def update_user_pool_client(self): + user_pool_id = self.parameters.pop("UserPoolId") + client_id = self.parameters.pop("ClientId") + user_pool_client = cognitoidp_backends[self.region].update_user_pool_client(user_pool_id, client_id, self.parameters) + return json.dumps({ + "UserPoolClient": user_pool_client.to_json(extended=True) + }) + + def delete_user_pool_client(self): + user_pool_id = self._get_param("UserPoolId") + client_id = self._get_param("ClientId") + cognitoidp_backends[self.region].delete_user_pool_client(user_pool_id, client_id) + return "" + + # Identity provider + def create_identity_provider(self): + user_pool_id = self._get_param("UserPoolId") + name = self.parameters.pop("ProviderName") + identity_provider = cognitoidp_backends[self.region].create_identity_provider(user_pool_id, name, self.parameters) + return json.dumps({ + "IdentityProvider": identity_provider.to_json(extended=True) + }) + + def list_identity_providers(self): + user_pool_id = self._get_param("UserPoolId") + identity_providers = cognitoidp_backends[self.region].list_identity_providers(user_pool_id) + return json.dumps({ + "Providers": [identity_provider.to_json() for identity_provider in identity_providers] + }) + + def describe_identity_provider(self): + user_pool_id = self._get_param("UserPoolId") + name = self._get_param("ProviderName") + identity_provider = cognitoidp_backends[self.region].describe_identity_provider(user_pool_id, name) + return json.dumps({ + "IdentityProvider": identity_provider.to_json(extended=True) + }) + + def delete_identity_provider(self): + user_pool_id = self._get_param("UserPoolId") + name = self._get_param("ProviderName") + cognitoidp_backends[self.region].delete_identity_provider(user_pool_id, name) + return "" + + # User + def admin_create_user(self): + user_pool_id = self._get_param("UserPoolId") + username = self._get_param("Username") + temporary_password = self._get_param("TemporaryPassword") + user = cognitoidp_backends[self.region].admin_create_user( + user_pool_id, + username, + temporary_password, + self._get_param("UserAttributes", []) + ) + + return json.dumps({ + "User": user.to_json(extended=True) + }) + + def admin_get_user(self): + user_pool_id = self._get_param("UserPoolId") + username = self._get_param("Username") + user = cognitoidp_backends[self.region].admin_get_user(user_pool_id, username) + return json.dumps( + user.to_json(extended=True, attributes_key="UserAttributes") + ) + + def list_users(self): + user_pool_id = self._get_param("UserPoolId") + users = cognitoidp_backends[self.region].list_users(user_pool_id) + return json.dumps({ + "Users": [user.to_json(extended=True) for user in users] + }) + + def admin_delete_user(self): + user_pool_id = self._get_param("UserPoolId") + username = self._get_param("Username") + cognitoidp_backends[self.region].admin_delete_user(user_pool_id, username) + return "" + + def admin_initiate_auth(self): + user_pool_id = self._get_param("UserPoolId") + client_id = self._get_param("ClientId") + auth_flow = self._get_param("AuthFlow") + auth_parameters = self._get_param("AuthParameters") + + auth_result = cognitoidp_backends[self.region].admin_initiate_auth( + user_pool_id, + client_id, + auth_flow, + auth_parameters, + ) + + return json.dumps(auth_result) + + def respond_to_auth_challenge(self): + session = self._get_param("Session") + client_id = self._get_param("ClientId") + challenge_name = self._get_param("ChallengeName") + challenge_responses = self._get_param("ChallengeResponses") + auth_result = cognitoidp_backends[self.region].respond_to_auth_challenge( + session, + client_id, + challenge_name, + challenge_responses, + ) + + return json.dumps(auth_result) + + def forgot_password(self): + return json.dumps({ + "CodeDeliveryDetails": { + "DeliveryMedium": "EMAIL", + "Destination": "...", + } + }) + + # This endpoint receives no authorization header, so if moto-server is listening + # on localhost (doesn't get a region in the host header), it doesn't know what + # region's backend should handle the traffic, and we use `find_region_by_value` to + # solve that problem. + def confirm_forgot_password(self): + client_id = self._get_param("ClientId") + username = self._get_param("Username") + password = self._get_param("Password") + region = find_region_by_value("client_id", client_id) + cognitoidp_backends[region].confirm_forgot_password(client_id, username, password) + return "" + + # Ditto the comment on confirm_forgot_password. + def change_password(self): + access_token = self._get_param("AccessToken") + previous_password = self._get_param("PreviousPassword") + proposed_password = self._get_param("ProposedPassword") + region = find_region_by_value("access_token", access_token) + cognitoidp_backends[region].change_password(access_token, previous_password, proposed_password) + return "" + + +class CognitoIdpJsonWebKeyResponse(BaseResponse): + + def __init__(self): + with open(os.path.join(os.path.dirname(__file__), "resources/jwks-public.json")) as f: + self.json_web_key = f.read() + + def serve_json_web_key(self, request, full_url, headers): + return 200, {"Content-Type": "application/json"}, self.json_web_key diff --git a/moto/cognitoidp/urls.py b/moto/cognitoidp/urls.py new file mode 100644 index 000000000..77441ed5e --- /dev/null +++ b/moto/cognitoidp/urls.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals +from .responses import CognitoIdpResponse, CognitoIdpJsonWebKeyResponse + +url_bases = [ + "https?://cognito-idp.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': CognitoIdpResponse.dispatch, + '{0}//.well-known/jwks.json$': CognitoIdpJsonWebKeyResponse().serve_json_web_key, +} diff --git a/moto/server.py b/moto/server.py index e9f4c0904..d6b0ee083 100644 --- a/moto/server.py +++ b/moto/server.py @@ -69,8 +69,13 @@ class DomainDispatcherApplication(object): _, _, region, service, _ = environ['HTTP_AUTHORIZATION'].split(",")[0].split()[ 1].split("/") except (KeyError, ValueError): + # Some cognito-idp endpoints (e.g. change password) do not receive an auth header. + if environ.get('HTTP_X_AMZ_TARGET', '').startswith('AWSCognitoIdentityProviderService'): + service = 'cognito-idp' + else: + service = 's3' + region = 'us-east-1' - service = 's3' if service == 'dynamodb': dynamo_api_version = environ['HTTP_X_AMZ_TARGET'].split("_")[1].split(".")[0] # If Newer API version, use dynamodb2 diff --git a/setup.py b/setup.py index ebbf6f0cd..8582e6b85 100755 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ install_requires = [ "pyaml", "pytz", "python-dateutil<3.0.0,>=2.1", + "python-jose<3.0.0", "mock", "docker>=2.5.1", "jsondiff==1.1.1", diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py new file mode 100644 index 000000000..b2bd469ce --- /dev/null +++ b/tests/test_cognitoidp/test_cognitoidp.py @@ -0,0 +1,539 @@ +from __future__ import unicode_literals + +import boto3 +import json +import os +import uuid + +from jose import jws +from moto import mock_cognitoidp +import sure # noqa + + +@mock_cognitoidp +def test_create_user_pool(): + conn = boto3.client("cognito-idp", "us-west-2") + + name = str(uuid.uuid4()) + value = str(uuid.uuid4()) + result = conn.create_user_pool( + PoolName=name, + LambdaConfig={ + "PreSignUp": value + } + ) + + result["UserPool"]["Id"].should_not.be.none + result["UserPool"]["Name"].should.equal(name) + result["UserPool"]["LambdaConfig"]["PreSignUp"].should.equal(value) + + +@mock_cognitoidp +def test_list_user_pools(): + conn = boto3.client("cognito-idp", "us-west-2") + + name = str(uuid.uuid4()) + conn.create_user_pool(PoolName=name) + result = conn.list_user_pools(MaxResults=10) + result["UserPools"].should.have.length_of(1) + result["UserPools"][0]["Name"].should.equal(name) + + +@mock_cognitoidp +def test_describe_user_pool(): + conn = boto3.client("cognito-idp", "us-west-2") + + name = str(uuid.uuid4()) + value = str(uuid.uuid4()) + user_pool_details = conn.create_user_pool( + PoolName=name, + LambdaConfig={ + "PreSignUp": value + } + ) + + result = conn.describe_user_pool(UserPoolId=user_pool_details["UserPool"]["Id"]) + result["UserPool"]["Name"].should.equal(name) + result["UserPool"]["LambdaConfig"]["PreSignUp"].should.equal(value) + + +@mock_cognitoidp +def test_delete_user_pool(): + conn = boto3.client("cognito-idp", "us-west-2") + + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.list_user_pools(MaxResults=10)["UserPools"].should.have.length_of(1) + conn.delete_user_pool(UserPoolId=user_pool_id) + conn.list_user_pools(MaxResults=10)["UserPools"].should.have.length_of(0) + + +@mock_cognitoidp +def test_create_user_pool_domain(): + conn = boto3.client("cognito-idp", "us-west-2") + + domain = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + result = conn.create_user_pool_domain(UserPoolId=user_pool_id, Domain=domain) + result["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + + +@mock_cognitoidp +def test_describe_user_pool_domain(): + conn = boto3.client("cognito-idp", "us-west-2") + + domain = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.create_user_pool_domain(UserPoolId=user_pool_id, Domain=domain) + result = conn.describe_user_pool_domain(Domain=domain) + result["DomainDescription"]["Domain"].should.equal(domain) + result["DomainDescription"]["UserPoolId"].should.equal(user_pool_id) + result["DomainDescription"]["AWSAccountId"].should_not.be.none + + +@mock_cognitoidp +def test_delete_user_pool_domain(): + conn = boto3.client("cognito-idp", "us-west-2") + + domain = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.create_user_pool_domain(UserPoolId=user_pool_id, Domain=domain) + result = conn.delete_user_pool_domain(UserPoolId=user_pool_id, Domain=domain) + result["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + result = conn.describe_user_pool_domain(Domain=domain) + # This is a surprising behavior of the real service: describing a missing domain comes + # back with status 200 and a DomainDescription of {} + result["ResponseMetadata"]["HTTPStatusCode"].should.equal(200) + result["DomainDescription"].keys().should.have.length_of(0) + + +@mock_cognitoidp +def test_create_user_pool_client(): + conn = boto3.client("cognito-idp", "us-west-2") + + client_name = str(uuid.uuid4()) + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + result = conn.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=client_name, + CallbackURLs=[value], + ) + + result["UserPoolClient"]["UserPoolId"].should.equal(user_pool_id) + result["UserPoolClient"]["ClientId"].should_not.be.none + result["UserPoolClient"]["ClientName"].should.equal(client_name) + result["UserPoolClient"]["CallbackURLs"].should.have.length_of(1) + result["UserPoolClient"]["CallbackURLs"][0].should.equal(value) + + +@mock_cognitoidp +def test_list_user_pool_clients(): + conn = boto3.client("cognito-idp", "us-west-2") + + client_name = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.create_user_pool_client(UserPoolId=user_pool_id, ClientName=client_name) + result = conn.list_user_pool_clients(UserPoolId=user_pool_id, MaxResults=10) + result["UserPoolClients"].should.have.length_of(1) + result["UserPoolClients"][0]["ClientName"].should.equal(client_name) + + +@mock_cognitoidp +def test_describe_user_pool_client(): + conn = boto3.client("cognito-idp", "us-west-2") + + client_name = str(uuid.uuid4()) + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + client_details = conn.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=client_name, + CallbackURLs=[value], + ) + + result = conn.describe_user_pool_client( + UserPoolId=user_pool_id, + ClientId=client_details["UserPoolClient"]["ClientId"], + ) + + result["UserPoolClient"]["ClientName"].should.equal(client_name) + result["UserPoolClient"]["CallbackURLs"].should.have.length_of(1) + result["UserPoolClient"]["CallbackURLs"][0].should.equal(value) + + +@mock_cognitoidp +def test_update_user_pool_client(): + conn = boto3.client("cognito-idp", "us-west-2") + + old_client_name = str(uuid.uuid4()) + new_client_name = str(uuid.uuid4()) + old_value = str(uuid.uuid4()) + new_value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + client_details = conn.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=old_client_name, + CallbackURLs=[old_value], + ) + + result = conn.update_user_pool_client( + UserPoolId=user_pool_id, + ClientId=client_details["UserPoolClient"]["ClientId"], + ClientName=new_client_name, + CallbackURLs=[new_value], + ) + + result["UserPoolClient"]["ClientName"].should.equal(new_client_name) + result["UserPoolClient"]["CallbackURLs"].should.have.length_of(1) + result["UserPoolClient"]["CallbackURLs"][0].should.equal(new_value) + + +@mock_cognitoidp +def test_delete_user_pool_client(): + conn = boto3.client("cognito-idp", "us-west-2") + + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + client_details = conn.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=str(uuid.uuid4()), + ) + + conn.delete_user_pool_client( + UserPoolId=user_pool_id, + ClientId=client_details["UserPoolClient"]["ClientId"], + ) + + caught = False + try: + conn.describe_user_pool_client( + UserPoolId=user_pool_id, + ClientId=client_details["UserPoolClient"]["ClientId"], + ) + except conn.exceptions.ResourceNotFoundException: + caught = True + + caught.should.be.true + + +@mock_cognitoidp +def test_create_identity_provider(): + conn = boto3.client("cognito-idp", "us-west-2") + + provider_name = str(uuid.uuid4()) + provider_type = "Facebook" + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + result = conn.create_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name, + ProviderType=provider_type, + ProviderDetails={ + "thing": value + }, + ) + + result["IdentityProvider"]["UserPoolId"].should.equal(user_pool_id) + result["IdentityProvider"]["ProviderName"].should.equal(provider_name) + result["IdentityProvider"]["ProviderType"].should.equal(provider_type) + result["IdentityProvider"]["ProviderDetails"]["thing"].should.equal(value) + + +@mock_cognitoidp +def test_list_identity_providers(): + conn = boto3.client("cognito-idp", "us-west-2") + + provider_name = str(uuid.uuid4()) + provider_type = "Facebook" + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.create_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name, + ProviderType=provider_type, + ProviderDetails={}, + ) + + result = conn.list_identity_providers( + UserPoolId=user_pool_id, + MaxResults=10, + ) + + result["Providers"].should.have.length_of(1) + result["Providers"][0]["ProviderName"].should.equal(provider_name) + result["Providers"][0]["ProviderType"].should.equal(provider_type) + + +@mock_cognitoidp +def test_describe_identity_providers(): + conn = boto3.client("cognito-idp", "us-west-2") + + provider_name = str(uuid.uuid4()) + provider_type = "Facebook" + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.create_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name, + ProviderType=provider_type, + ProviderDetails={ + "thing": value + }, + ) + + result = conn.describe_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name, + ) + + result["IdentityProvider"]["UserPoolId"].should.equal(user_pool_id) + result["IdentityProvider"]["ProviderName"].should.equal(provider_name) + result["IdentityProvider"]["ProviderType"].should.equal(provider_type) + result["IdentityProvider"]["ProviderDetails"]["thing"].should.equal(value) + + +@mock_cognitoidp +def test_delete_identity_providers(): + conn = boto3.client("cognito-idp", "us-west-2") + + provider_name = str(uuid.uuid4()) + provider_type = "Facebook" + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.create_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name, + ProviderType=provider_type, + ProviderDetails={ + "thing": value + }, + ) + + conn.delete_identity_provider(UserPoolId=user_pool_id, ProviderName=provider_name) + + caught = False + try: + conn.describe_identity_provider( + UserPoolId=user_pool_id, + ProviderName=provider_name, + ) + except conn.exceptions.ResourceNotFoundException: + caught = True + + caught.should.be.true + + +@mock_cognitoidp +def test_admin_create_user(): + conn = boto3.client("cognito-idp", "us-west-2") + + username = str(uuid.uuid4()) + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + result = conn.admin_create_user( + UserPoolId=user_pool_id, + Username=username, + UserAttributes=[ + {"Name": "thing", "Value": value} + ], + ) + + result["User"]["Username"].should.equal(username) + result["User"]["UserStatus"].should.equal("FORCE_CHANGE_PASSWORD") + result["User"]["Attributes"].should.have.length_of(1) + result["User"]["Attributes"][0]["Name"].should.equal("thing") + result["User"]["Attributes"][0]["Value"].should.equal(value) + + +@mock_cognitoidp +def test_admin_get_user(): + conn = boto3.client("cognito-idp", "us-west-2") + + username = str(uuid.uuid4()) + value = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.admin_create_user( + UserPoolId=user_pool_id, + Username=username, + UserAttributes=[ + {"Name": "thing", "Value": value} + ], + ) + + result = conn.admin_get_user(UserPoolId=user_pool_id, Username=username) + result["Username"].should.equal(username) + result["UserAttributes"].should.have.length_of(1) + result["UserAttributes"][0]["Name"].should.equal("thing") + result["UserAttributes"][0]["Value"].should.equal(value) + + +@mock_cognitoidp +def test_list_users(): + conn = boto3.client("cognito-idp", "us-west-2") + + username = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.admin_create_user(UserPoolId=user_pool_id, Username=username) + result = conn.list_users(UserPoolId=user_pool_id) + result["Users"].should.have.length_of(1) + result["Users"][0]["Username"].should.equal(username) + + +@mock_cognitoidp +def test_admin_delete_user(): + conn = boto3.client("cognito-idp", "us-west-2") + + username = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + conn.admin_create_user(UserPoolId=user_pool_id, Username=username) + conn.admin_delete_user(UserPoolId=user_pool_id, Username=username) + + caught = False + try: + conn.admin_get_user(UserPoolId=user_pool_id, Username=username) + except conn.exceptions.ResourceNotFoundException: + caught = True + + caught.should.be.true + + +def authentication_flow(conn): + username = str(uuid.uuid4()) + temporary_password = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + client_id = conn.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=str(uuid.uuid4()), + )["UserPoolClient"]["ClientId"] + + conn.admin_create_user( + UserPoolId=user_pool_id, + Username=username, + TemporaryPassword=temporary_password, + ) + + result = conn.admin_initiate_auth( + UserPoolId=user_pool_id, + ClientId=client_id, + AuthFlow="ADMIN_NO_SRP_AUTH", + AuthParameters={ + "USERNAME": username, + "PASSWORD": temporary_password + }, + ) + + # A newly created user is forced to set a new password + result["ChallengeName"].should.equal("NEW_PASSWORD_REQUIRED") + result["Session"].should_not.be.none + + # This sets a new password and logs the user in (creates tokens) + new_password = str(uuid.uuid4()) + result = conn.respond_to_auth_challenge( + Session=result["Session"], + ClientId=client_id, + ChallengeName="NEW_PASSWORD_REQUIRED", + ChallengeResponses={ + "USERNAME": username, + "NEW_PASSWORD": new_password + } + ) + + result["AuthenticationResult"]["IdToken"].should_not.be.none + result["AuthenticationResult"]["AccessToken"].should_not.be.none + + return { + "user_pool_id": user_pool_id, + "client_id": client_id, + "id_token": result["AuthenticationResult"]["IdToken"], + "access_token": result["AuthenticationResult"]["AccessToken"], + "username": username, + "password": new_password, + } + + +@mock_cognitoidp +def test_authentication_flow(): + conn = boto3.client("cognito-idp", "us-west-2") + + authentication_flow(conn) + + +@mock_cognitoidp +def test_token_legitimacy(): + conn = boto3.client("cognito-idp", "us-west-2") + + path = "../../moto/cognitoidp/resources/jwks-public.json" + with open(os.path.join(os.path.dirname(__file__), path)) as f: + json_web_key = json.loads(f.read())["keys"][0] + + outputs = authentication_flow(conn) + id_token = outputs["id_token"] + access_token = outputs["access_token"] + client_id = outputs["client_id"] + issuer = "https://cognito-idp.us-west-2.amazonaws.com/{}".format(outputs["user_pool_id"]) + id_claims = json.loads(jws.verify(id_token, json_web_key, "RS256")) + id_claims["iss"].should.equal(issuer) + id_claims["aud"].should.equal(client_id) + access_claims = json.loads(jws.verify(access_token, json_web_key, "RS256")) + access_claims["iss"].should.equal(issuer) + access_claims["aud"].should.equal(client_id) + + +@mock_cognitoidp +def test_change_password(): + conn = boto3.client("cognito-idp", "us-west-2") + + outputs = authentication_flow(conn) + + # Take this opportunity to test change_password, which requires an access token. + newer_password = str(uuid.uuid4()) + conn.change_password( + AccessToken=outputs["access_token"], + PreviousPassword=outputs["password"], + ProposedPassword=newer_password, + ) + + # Log in again, which should succeed without a challenge because the user is no + # longer in the force-new-password state. + result = conn.admin_initiate_auth( + UserPoolId=outputs["user_pool_id"], + ClientId=outputs["client_id"], + AuthFlow="ADMIN_NO_SRP_AUTH", + AuthParameters={ + "USERNAME": outputs["username"], + "PASSWORD": newer_password, + }, + ) + + result["AuthenticationResult"].should_not.be.none + + +@mock_cognitoidp +def test_forgot_password(): + conn = boto3.client("cognito-idp", "us-west-2") + + result = conn.forgot_password(ClientId=str(uuid.uuid4()), Username=str(uuid.uuid4())) + result["CodeDeliveryDetails"].should_not.be.none + + +@mock_cognitoidp +def test_confirm_forgot_password(): + conn = boto3.client("cognito-idp", "us-west-2") + + username = str(uuid.uuid4()) + user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"] + client_id = conn.create_user_pool_client( + UserPoolId=user_pool_id, + ClientName=str(uuid.uuid4()), + )["UserPoolClient"]["ClientId"] + + conn.admin_create_user( + UserPoolId=user_pool_id, + Username=username, + TemporaryPassword=str(uuid.uuid4()), + ) + + conn.confirm_forgot_password( + ClientId=client_id, + Username=username, + ConfirmationCode=str(uuid.uuid4()), + Password=str(uuid.uuid4()), + )