1# Copyright (c) 2014 Amazon.com, Inc. or its affiliates.  All Rights Reserved
2#
3# Permission is hereby granted, free of charge, to any person obtaining a
4# copy of this software and associated documentation files (the
5# "Software"), to deal in the Software without restriction, including
6# without limitation the rights to use, copy, modify, merge, publish, dis-
7# tribute, sublicense, and/or sell copies of the Software, and to permit
8# persons to whom the Software is furnished to do so, subject to the fol-
9# lowing conditions:
10#
11# The above copyright notice and this permission notice shall be included
12# in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21#
22
23import boto
24from boto.compat import json
25from boto.connection import AWSQueryConnection
26from boto.regioninfo import RegionInfo
27from boto.exception import JSONResponseError
28from boto.cognito.identity import exceptions
29
30
31class CognitoIdentityConnection(AWSQueryConnection):
32    """
33    Amazon Cognito
34    Amazon Cognito is a web service that delivers scoped temporary
35    credentials to mobile devices and other untrusted environments.
36    Amazon Cognito uniquely identifies a device and supplies the user
37    with a consistent identity over the lifetime of an application.
38
39    Using Amazon Cognito, you can enable authentication with one or
40    more third-party identity providers (Facebook, Google, or Login
41    with Amazon), and you can also choose to support unauthenticated
42    access from your app. Cognito delivers a unique identifier for
43    each user and acts as an OpenID token provider trusted by AWS
44    Security Token Service (STS) to access temporary, limited-
45    privilege AWS credentials.
46
47    To provide end-user credentials, first make an unsigned call to
48    GetId. If the end user is authenticated with one of the supported
49    identity providers, set the `Logins` map with the identity
50    provider token. `GetId` returns a unique identifier for the user.
51
52    Next, make an unsigned call to GetOpenIdToken, which returns the
53    OpenID token necessary to call STS and retrieve AWS credentials.
54    This call expects the same `Logins` map as the `GetId` call, as
55    well as the `IdentityID` originally returned by `GetId`. The token
56    returned by `GetOpenIdToken` can be passed to the STS operation
57    `AssumeRoleWithWebIdentity`_ to retrieve AWS credentials.
58    """
59    APIVersion = "2014-06-30"
60    DefaultRegionName = "us-east-1"
61    DefaultRegionEndpoint = "cognito-identity.us-east-1.amazonaws.com"
62    ServiceName = "CognitoIdentity"
63    TargetPrefix = "AWSCognitoIdentityService"
64    ResponseError = JSONResponseError
65
66    _faults = {
67        "LimitExceededException": exceptions.LimitExceededException,
68        "ResourceConflictException": exceptions.ResourceConflictException,
69        "DeveloperUserAlreadyRegisteredException": exceptions.DeveloperUserAlreadyRegisteredException,
70        "TooManyRequestsException": exceptions.TooManyRequestsException,
71        "InvalidParameterException": exceptions.InvalidParameterException,
72        "ResourceNotFoundException": exceptions.ResourceNotFoundException,
73        "InternalErrorException": exceptions.InternalErrorException,
74        "NotAuthorizedException": exceptions.NotAuthorizedException,
75    }
76
77
78    def __init__(self, **kwargs):
79        region = kwargs.pop('region', None)
80        if not region:
81            region = RegionInfo(self, self.DefaultRegionName,
82                                self.DefaultRegionEndpoint)
83
84        if 'host' not in kwargs or kwargs['host'] is None:
85            kwargs['host'] = region.endpoint
86
87        super(CognitoIdentityConnection, self).__init__(**kwargs)
88        self.region = region
89
90    def _required_auth_capability(self):
91        return ['hmac-v4']
92
93    def create_identity_pool(self, identity_pool_name,
94                             allow_unauthenticated_identities,
95                             supported_login_providers=None,
96                             developer_provider_name=None,
97                             open_id_connect_provider_ar_ns=None):
98        """
99        Creates a new identity pool. The identity pool is a store of
100        user identity information that is specific to your AWS
101        account. The limit on identity pools is 60 per account.
102
103        :type identity_pool_name: string
104        :param identity_pool_name: A string that you provide.
105
106        :type allow_unauthenticated_identities: boolean
107        :param allow_unauthenticated_identities: TRUE if the identity pool
108            supports unauthenticated logins.
109
110        :type supported_login_providers: map
111        :param supported_login_providers: Optional key:value pairs mapping
112            provider names to provider app IDs.
113
114        :type developer_provider_name: string
115        :param developer_provider_name: The "domain" by which Cognito will
116            refer to your users. This name acts as a placeholder that allows
117            your backend and the Cognito service to communicate about the
118            developer provider. For the `DeveloperProviderName`, you can use
119            letters as well as period ( `.`), underscore ( `_`), and dash (
120            `-`).
121        Once you have set a developer provider name, you cannot change it.
122            Please take care in setting this parameter.
123
124        :type open_id_connect_provider_ar_ns: list
125        :param open_id_connect_provider_ar_ns:
126
127        """
128        params = {
129            'IdentityPoolName': identity_pool_name,
130            'AllowUnauthenticatedIdentities': allow_unauthenticated_identities,
131        }
132        if supported_login_providers is not None:
133            params['SupportedLoginProviders'] = supported_login_providers
134        if developer_provider_name is not None:
135            params['DeveloperProviderName'] = developer_provider_name
136        if open_id_connect_provider_ar_ns is not None:
137            params['OpenIdConnectProviderARNs'] = open_id_connect_provider_ar_ns
138        return self.make_request(action='CreateIdentityPool',
139                                 body=json.dumps(params))
140
141    def delete_identity_pool(self, identity_pool_id):
142        """
143        Deletes a user pool. Once a pool is deleted, users will not be
144        able to authenticate with the pool.
145
146        :type identity_pool_id: string
147        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
148
149        """
150        params = {'IdentityPoolId': identity_pool_id, }
151        return self.make_request(action='DeleteIdentityPool',
152                                 body=json.dumps(params))
153
154    def describe_identity_pool(self, identity_pool_id):
155        """
156        Gets details about a particular identity pool, including the
157        pool name, ID description, creation date, and current number
158        of users.
159
160        :type identity_pool_id: string
161        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
162
163        """
164        params = {'IdentityPoolId': identity_pool_id, }
165        return self.make_request(action='DescribeIdentityPool',
166                                 body=json.dumps(params))
167
168    def get_id(self, account_id, identity_pool_id, logins=None):
169        """
170        Generates (or retrieves) a Cognito ID. Supplying multiple
171        logins will create an implicit linked account.
172
173        :type account_id: string
174        :param account_id: A standard AWS account ID (9+ digits).
175
176        :type identity_pool_id: string
177        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
178
179        :type logins: map
180        :param logins: A set of optional name-value pairs that map provider
181            names to provider tokens.
182        The available provider names for `Logins` are as follows:
183
184        + Facebook: `graph.facebook.com`
185        + Google: `accounts.google.com`
186        + Amazon: `www.amazon.com`
187
188        """
189        params = {
190            'AccountId': account_id,
191            'IdentityPoolId': identity_pool_id,
192        }
193        if logins is not None:
194            params['Logins'] = logins
195        return self.make_request(action='GetId',
196                                 body=json.dumps(params))
197
198    def get_open_id_token(self, identity_id, logins=None):
199        """
200        Gets an OpenID token, using a known Cognito ID. This known
201        Cognito ID is returned by GetId. You can optionally add
202        additional logins for the identity. Supplying multiple logins
203        creates an implicit link.
204
205        The OpenId token is valid for 15 minutes.
206
207        :type identity_id: string
208        :param identity_id: A unique identifier in the format REGION:GUID.
209
210        :type logins: map
211        :param logins: A set of optional name-value pairs that map provider
212            names to provider tokens.
213
214        """
215        params = {'IdentityId': identity_id, }
216        if logins is not None:
217            params['Logins'] = logins
218        return self.make_request(action='GetOpenIdToken',
219                                 body=json.dumps(params))
220
221    def get_open_id_token_for_developer_identity(self, identity_pool_id,
222                                                 logins, identity_id=None,
223                                                 token_duration=None):
224        """
225        Registers (or retrieves) a Cognito `IdentityId` and an OpenID
226        Connect token for a user authenticated by your backend
227        authentication process. Supplying multiple logins will create
228        an implicit linked account. You can only specify one developer
229        provider as part of the `Logins` map, which is linked to the
230        identity pool. The developer provider is the "domain" by which
231        Cognito will refer to your users.
232
233        You can use `GetOpenIdTokenForDeveloperIdentity` to create a
234        new identity and to link new logins (that is, user credentials
235        issued by a public provider or developer provider) to an
236        existing identity. When you want to create a new identity, the
237        `IdentityId` should be null. When you want to associate a new
238        login with an existing authenticated/unauthenticated identity,
239        you can do so by providing the existing `IdentityId`. This API
240        will create the identity in the specified `IdentityPoolId`.
241
242        :type identity_pool_id: string
243        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
244
245        :type identity_id: string
246        :param identity_id: A unique identifier in the format REGION:GUID.
247
248        :type logins: map
249        :param logins: A set of optional name-value pairs that map provider
250            names to provider tokens. Each name-value pair represents a user
251            from a public provider or developer provider. If the user is from a
252            developer provider, the name-value pair will follow the syntax
253            `"developer_provider_name": "developer_user_identifier"`. The
254            developer provider is the "domain" by which Cognito will refer to
255            your users; you provided this domain while creating/updating the
256            identity pool. The developer user identifier is an identifier from
257            your backend that uniquely identifies a user. When you create an
258            identity pool, you can specify the supported logins.
259
260        :type token_duration: long
261        :param token_duration: The expiration time of the token, in seconds.
262            You can specify a custom expiration time for the token so that you
263            can cache it. If you don't provide an expiration time, the token is
264            valid for 15 minutes. You can exchange the token with Amazon STS
265            for temporary AWS credentials, which are valid for a maximum of one
266            hour. The maximum token duration you can set is 24 hours. You
267            should take care in setting the expiration time for a token, as
268            there are significant security implications: an attacker could use
269            a leaked token to access your AWS resources for the token's
270            duration.
271
272        """
273        params = {
274            'IdentityPoolId': identity_pool_id,
275            'Logins': logins,
276        }
277        if identity_id is not None:
278            params['IdentityId'] = identity_id
279        if token_duration is not None:
280            params['TokenDuration'] = token_duration
281        return self.make_request(action='GetOpenIdTokenForDeveloperIdentity',
282                                 body=json.dumps(params))
283
284    def list_identities(self, identity_pool_id, max_results, next_token=None):
285        """
286        Lists the identities in a pool.
287
288        :type identity_pool_id: string
289        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
290
291        :type max_results: integer
292        :param max_results: The maximum number of identities to return.
293
294        :type next_token: string
295        :param next_token: A pagination token.
296
297        """
298        params = {
299            'IdentityPoolId': identity_pool_id,
300            'MaxResults': max_results,
301        }
302        if next_token is not None:
303            params['NextToken'] = next_token
304        return self.make_request(action='ListIdentities',
305                                 body=json.dumps(params))
306
307    def list_identity_pools(self, max_results, next_token=None):
308        """
309        Lists all of the Cognito identity pools registered for your
310        account.
311
312        :type max_results: integer
313        :param max_results: The maximum number of identities to return.
314
315        :type next_token: string
316        :param next_token: A pagination token.
317
318        """
319        params = {'MaxResults': max_results, }
320        if next_token is not None:
321            params['NextToken'] = next_token
322        return self.make_request(action='ListIdentityPools',
323                                 body=json.dumps(params))
324
325    def lookup_developer_identity(self, identity_pool_id, identity_id=None,
326                                  developer_user_identifier=None,
327                                  max_results=None, next_token=None):
328        """
329        Retrieves the `IdentityID` associated with a
330        `DeveloperUserIdentifier` or the list of
331        `DeveloperUserIdentifier`s associated with an `IdentityId` for
332        an existing identity. Either `IdentityID` or
333        `DeveloperUserIdentifier` must not be null. If you supply only
334        one of these values, the other value will be searched in the
335        database and returned as a part of the response. If you supply
336        both, `DeveloperUserIdentifier` will be matched against
337        `IdentityID`. If the values are verified against the database,
338        the response returns both values and is the same as the
339        request. Otherwise a `ResourceConflictException` is thrown.
340
341        :type identity_pool_id: string
342        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
343
344        :type identity_id: string
345        :param identity_id: A unique identifier in the format REGION:GUID.
346
347        :type developer_user_identifier: string
348        :param developer_user_identifier: A unique ID used by your backend
349            authentication process to identify a user. Typically, a developer
350            identity provider would issue many developer user identifiers, in
351            keeping with the number of users.
352
353        :type max_results: integer
354        :param max_results: The maximum number of identities to return.
355
356        :type next_token: string
357        :param next_token: A pagination token. The first call you make will
358            have `NextToken` set to null. After that the service will return
359            `NextToken` values as needed. For example, let's say you make a
360            request with `MaxResults` set to 10, and there are 20 matches in
361            the database. The service will return a pagination token as a part
362            of the response. This token can be used to call the API again and
363            get results starting from the 11th match.
364
365        """
366        params = {'IdentityPoolId': identity_pool_id, }
367        if identity_id is not None:
368            params['IdentityId'] = identity_id
369        if developer_user_identifier is not None:
370            params['DeveloperUserIdentifier'] = developer_user_identifier
371        if max_results is not None:
372            params['MaxResults'] = max_results
373        if next_token is not None:
374            params['NextToken'] = next_token
375        return self.make_request(action='LookupDeveloperIdentity',
376                                 body=json.dumps(params))
377
378    def merge_developer_identities(self, source_user_identifier,
379                                   destination_user_identifier,
380                                   developer_provider_name, identity_pool_id):
381        """
382        Merges two users having different `IdentityId`s, existing in
383        the same identity pool, and identified by the same developer
384        provider. You can use this action to request that discrete
385        users be merged and identified as a single user in the Cognito
386        environment. Cognito associates the given source user (
387        `SourceUserIdentifier`) with the `IdentityId` of the
388        `DestinationUserIdentifier`. Only developer-authenticated
389        users can be merged. If the users to be merged are associated
390        with the same public provider, but as two different users, an
391        exception will be thrown.
392
393        :type source_user_identifier: string
394        :param source_user_identifier: User identifier for the source user. The
395            value should be a `DeveloperUserIdentifier`.
396
397        :type destination_user_identifier: string
398        :param destination_user_identifier: User identifier for the destination
399            user. The value should be a `DeveloperUserIdentifier`.
400
401        :type developer_provider_name: string
402        :param developer_provider_name: The "domain" by which Cognito will
403            refer to your users. This is a (pseudo) domain name that you
404            provide while creating an identity pool. This name acts as a
405            placeholder that allows your backend and the Cognito service to
406            communicate about the developer provider. For the
407            `DeveloperProviderName`, you can use letters as well as period (.),
408            underscore (_), and dash (-).
409
410        :type identity_pool_id: string
411        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
412
413        """
414        params = {
415            'SourceUserIdentifier': source_user_identifier,
416            'DestinationUserIdentifier': destination_user_identifier,
417            'DeveloperProviderName': developer_provider_name,
418            'IdentityPoolId': identity_pool_id,
419        }
420        return self.make_request(action='MergeDeveloperIdentities',
421                                 body=json.dumps(params))
422
423    def unlink_developer_identity(self, identity_id, identity_pool_id,
424                                  developer_provider_name,
425                                  developer_user_identifier):
426        """
427        Unlinks a `DeveloperUserIdentifier` from an existing identity.
428        Unlinked developer users will be considered new identities
429        next time they are seen. If, for a given Cognito identity, you
430        remove all federated identities as well as the developer user
431        identifier, the Cognito identity becomes inaccessible.
432
433        :type identity_id: string
434        :param identity_id: A unique identifier in the format REGION:GUID.
435
436        :type identity_pool_id: string
437        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
438
439        :type developer_provider_name: string
440        :param developer_provider_name: The "domain" by which Cognito will
441            refer to your users.
442
443        :type developer_user_identifier: string
444        :param developer_user_identifier: A unique ID used by your backend
445            authentication process to identify a user.
446
447        """
448        params = {
449            'IdentityId': identity_id,
450            'IdentityPoolId': identity_pool_id,
451            'DeveloperProviderName': developer_provider_name,
452            'DeveloperUserIdentifier': developer_user_identifier,
453        }
454        return self.make_request(action='UnlinkDeveloperIdentity',
455                                 body=json.dumps(params))
456
457    def unlink_identity(self, identity_id, logins, logins_to_remove):
458        """
459        Unlinks a federated identity from an existing account.
460        Unlinked logins will be considered new identities next time
461        they are seen. Removing the last linked login will make this
462        identity inaccessible.
463
464        :type identity_id: string
465        :param identity_id: A unique identifier in the format REGION:GUID.
466
467        :type logins: map
468        :param logins: A set of optional name-value pairs that map provider
469            names to provider tokens.
470
471        :type logins_to_remove: list
472        :param logins_to_remove: Provider names to unlink from this identity.
473
474        """
475        params = {
476            'IdentityId': identity_id,
477            'Logins': logins,
478            'LoginsToRemove': logins_to_remove,
479        }
480        return self.make_request(action='UnlinkIdentity',
481                                 body=json.dumps(params))
482
483    def update_identity_pool(self, identity_pool_id, identity_pool_name,
484                             allow_unauthenticated_identities,
485                             supported_login_providers=None,
486                             developer_provider_name=None,
487                             open_id_connect_provider_ar_ns=None):
488        """
489        Updates a user pool.
490
491        :type identity_pool_id: string
492        :param identity_pool_id: An identity pool ID in the format REGION:GUID.
493
494        :type identity_pool_name: string
495        :param identity_pool_name: A string that you provide.
496
497        :type allow_unauthenticated_identities: boolean
498        :param allow_unauthenticated_identities: TRUE if the identity pool
499            supports unauthenticated logins.
500
501        :type supported_login_providers: map
502        :param supported_login_providers: Optional key:value pairs mapping
503            provider names to provider app IDs.
504
505        :type developer_provider_name: string
506        :param developer_provider_name: The "domain" by which Cognito will
507            refer to your users.
508
509        :type open_id_connect_provider_ar_ns: list
510        :param open_id_connect_provider_ar_ns:
511
512        """
513        params = {
514            'IdentityPoolId': identity_pool_id,
515            'IdentityPoolName': identity_pool_name,
516            'AllowUnauthenticatedIdentities': allow_unauthenticated_identities,
517        }
518        if supported_login_providers is not None:
519            params['SupportedLoginProviders'] = supported_login_providers
520        if developer_provider_name is not None:
521            params['DeveloperProviderName'] = developer_provider_name
522        if open_id_connect_provider_ar_ns is not None:
523            params['OpenIdConnectProviderARNs'] = open_id_connect_provider_ar_ns
524        return self.make_request(action='UpdateIdentityPool',
525                                 body=json.dumps(params))
526
527    def make_request(self, action, body):
528        headers = {
529            'X-Amz-Target': '%s.%s' % (self.TargetPrefix, action),
530            'Host': self.region.endpoint,
531            'Content-Type': 'application/x-amz-json-1.1',
532            'Content-Length': str(len(body)),
533        }
534        http_request = self.build_base_http_request(
535            method='POST', path='/', auth_path='/', params={},
536            headers=headers, data=body)
537        response = self._mexe(http_request, sender=None,
538                              override_num_retries=10)
539        response_body = response.read().decode('utf-8')
540        boto.log.debug(response_body)
541        if response.status == 200:
542            if response_body:
543                return json.loads(response_body)
544        else:
545            json_body = json.loads(response_body)
546            fault_name = json_body.get('__type', None)
547            exception_class = self._faults.get(fault_name, self.ResponseError)
548            raise exception_class(response.status, response.reason,
549                                  body=json_body)
550