pax_global_header00006660000000000000000000000064146213541760014523gustar00rootroot0000000000000052 comment=4f6c90a455c2d63b3014492e1c08ad1b82c886e3 NabuCasa-pycognito-4f6c90a/000077500000000000000000000000001462135417600156275ustar00rootroot00000000000000NabuCasa-pycognito-4f6c90a/.devcontainer.json000066400000000000000000000022501462135417600212600ustar00rootroot00000000000000{ "name": "pycognito dev", "image": "mcr.microsoft.com/vscode/devcontainers/python:0-3.8", "postStartCommand": "python3 -m pip install -e .", "postCreateCommand": "python3 -m pip install -r requirements_test.txt tox", "containerUser": "vscode", "containerEnv": { "GIT_EDITOR": "code --wait" }, "customizations": { "vscode": { "extensions": [ "esbenp.prettier-vscode", "ms-python.python", "ms-python.vscode-pylance", "visualstudioexptteam.vscodeintellicode" ], "settings": { "python.pythonPath": "/usr/local/bin/python", "python.formatting.provider": "black", "editor.formatOnPaste": false, "editor.formatOnSave": true, "editor.formatOnType": true, "editor.rulers": [ 88 ], "editor.codeActionsOnSave": { "source.fixAll": "always", "source.organizeImports": "always" }, "files.trimTrailingWhitespace": true, "terminal.integrated.profiles.linux": { "zsh": { "path": "/usr/bin/zsh" } }, "terminal.integrated.defaultProfile.linux": "zsh" } } } }NabuCasa-pycognito-4f6c90a/.github/000077500000000000000000000000001462135417600171675ustar00rootroot00000000000000NabuCasa-pycognito-4f6c90a/.github/FUNDING.yml000066400000000000000000000001031462135417600207760ustar00rootroot00000000000000# These are supported funding model platforms github: [pvizeli,] NabuCasa-pycognito-4f6c90a/.github/ISSUE_TEMPLATE/000077500000000000000000000000001462135417600213525ustar00rootroot00000000000000NabuCasa-pycognito-4f6c90a/.github/ISSUE_TEMPLATE/bug.yml000066400000000000000000000017741462135417600226630ustar00rootroot00000000000000name: "Report a bug with pyCognito" description: Report an issue with pyCognito body: - type: textarea attributes: label: The problem placeholder: >- Describe the issue you are experiencing here to communicate to the maintainers. Tell us what you were trying to do and what happened. validations: required: true - type: markdown attributes: value: | ## Environment - type: input attributes: label: Operating system validations: required: true - type: textarea validations: required: true attributes: label: Python version render: txt - type: textarea validations: required: true attributes: label: Problem-relevant code description: >- A minimal example to reproduce the bug render: python - type: textarea attributes: label: Traceback/Error logs description: >- If you come across any trace or error logs, please provide them. render: txt - type: textarea attributes: label: Additional information NabuCasa-pycognito-4f6c90a/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000331462135417600233360ustar00rootroot00000000000000blank_issues_enabled: falseNabuCasa-pycognito-4f6c90a/.github/dependabot.yml000066400000000000000000000003671462135417600220250ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: pip directory: "/" schedule: interval: monthly open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: monthly open-pull-requests-limit: 10 NabuCasa-pycognito-4f6c90a/.github/release-drafter.yml000066400000000000000000000003241462135417600227560ustar00rootroot00000000000000change-template: "- #$NUMBER $TITLE @$AUTHOR" sort-direction: ascending categories: - title: "Dependency Updates" label: "dependencies" collapse-after: 1 template: | ## What’s Changed $CHANGES NabuCasa-pycognito-4f6c90a/.github/workflows/000077500000000000000000000000001462135417600212245ustar00rootroot00000000000000NabuCasa-pycognito-4f6c90a/.github/workflows/ci.yml000066400000000000000000000012051462135417600223400ustar00rootroot00000000000000name: CI on: push: branches: - master pull_request: branches: - master jobs: tox: runs-on: ubuntu-latest name: Tox strategy: fail-fast: false matrix: python-version: - "3.8" - "3.9" - "3.10" - "3.11" - "3.12" steps: - uses: actions/checkout@v4.1.5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5.1.0 with: python-version: ${{ matrix.python-version }} - name: Install tox run: python3 -m pip install tox - name: Run Tox run: tox NabuCasa-pycognito-4f6c90a/.github/workflows/pythonpublish.yml000066400000000000000000000022761462135417600246660ustar00rootroot00000000000000name: Upload Python Package on: release: types: - published permissions: {} jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.5 - name: Set up Python uses: actions/setup-python@v5.1.0 with: python-version: "3.x" - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools build - name: Build run: | python -m build - name: Upload dists uses: actions/upload-artifact@v4.3.3 with: name: "dist" path: "dist/" if-no-files-found: error retention-days: 5 publish: name: Upload release to PyPI runs-on: ubuntu-latest needs: "build" environment: name: release url: https://pypi.org/p/pycognito permissions: id-token: write steps: - name: Download dists uses: actions/download-artifact@v4.1.7 with: name: "dist" path: "dist/" - name: Publish dists to PyPI # Pinned to a commit for security purposes uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 NabuCasa-pycognito-4f6c90a/.github/workflows/release-drafter.yml000066400000000000000000000004761462135417600250230ustar00rootroot00000000000000name: Release Drafter on: push: # branches to consider in the event; optional, defaults to all branches: - master jobs: update_release_draft: runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v6.0.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NabuCasa-pycognito-4f6c90a/.gitignore000066400000000000000000000023011462135417600176130ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .idea/vcs.xmlNabuCasa-pycognito-4f6c90a/LICENSE000066400000000000000000000261351462135417600166430ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. NabuCasa-pycognito-4f6c90a/README.md000066400000000000000000000566751462135417600171310ustar00rootroot00000000000000# pyCognito Makes working with AWS Cognito easier for Python developers. ## Getting Started - [Python Versions Supported](#python-versions-supported) - [Install](#install) - [Environment Variables](#environment-variables) - [COGNITO_JWKS](#cognito-jwks) (optional) - [Cognito Utility Class](#cognito-utility-class) `pycognito.Cognito` - [Cognito Methods](#cognito-methods) - [Register](#register) - [Authenticate](#authenticate) - [Admin Authenticate](#admin-authenticate) - [Initiate Forgot Password](#initiate-forgot-password) - [Confirm Forgot Password](#confirm-forgot-password) - [Change Password](#change-password) - [Confirm Sign Up](#confirm-sign-up) - [Update Profile](#update-profile) - [Send Verification](#send-verification) - [Get User Object](#get-user-object) - [Get User](#get-user) - [Get Users](#get-users) - [Get Group Object](#get-group-object) - [Get Group](#get-group) - [Get Groups](#get-groups) - [Check Token](#check-token) - [Verify Tokens](#verify-tokens) - [Logout](#logout) - [Associate Software Token](#associate-software-token) - [Verify Software Token](#verify-software-token) - [Set User MFA Preference](#set-user-mfa-preference) - [Respond to Software Token MFA challenge](#respond-to-software-token-mfa-challenge) - [Respond to SMS MFA challenge](#respond-to-sms-mfa-challenge) - [Cognito SRP Utility](#cognito-srp-utility) - [Using AWSSRP](#using-awssrp) - [Device Authentication Support](#device-authentication-support) - [Receiving DeviceKey and DeviceGroupKey](#receiving-devicekey-and-devicegroupkey) - [Confirming a Device](#confirming-a-device) - [Updating Device Status](#updating-device-status) - [Authenticating your Device](#authenticating-your-device) - [Forget Device](#forget-device) - [SRP Requests Authenticator](#srp-requests-authenticator) ## Python Versions Supported - 3.8 - 3.9 - 3.10 - 3.11 - 3.12 ## Install `pip install pycognito` ## Environment Variables #### COGNITO_JWKS **Optional:** This environment variable is a dictionary that represent the well known JWKs assigned to your user pool by AWS Cognito. You can find the keys for your user pool by substituting in your AWS region and pool id for the following example. `https://cognito-idp.{aws-region}.amazonaws.com/{user-pool-id}/.well-known/jwks.json` **Example Value (Not Real):** ```commandline COGNITO_JWKS={"keys": [{"alg": "RS256","e": "AQAB","kid": "123456789ABCDEFGHIJKLMNOP","kty": "RSA","n": "123456789ABCDEFGHIJKLMNOP","use": "sig"},{"alg": "RS256","e": "AQAB","kid": "123456789ABCDEFGHIJKLMNOP","kty": "RSA","n": "123456789ABCDEFGHIJKLMNOP","use": "sig"}]} ``` ## Cognito Utility Class ### Example with All Arguments ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', client_secret='optional-client-secret' username='optional-username', id_token='optional-id-token', refresh_token='optional-refresh-token', access_token='optional-access-token', access_key='optional-access-key', secret_key='optional-secret-key') ``` #### Arguments - **user_pool_id:** Cognito User Pool ID - **client_id:** Cognito User Pool Application client ID - **client_secret:** App client secret (if app client is configured with client secret) - **username:** User Pool username - **id_token:** ID Token returned by authentication - **refresh_token:** Refresh Token returned by authentication - **access_token:** Access Token returned by authentication - **access_key:** AWS IAM access key - **secret_key:** AWS IAM secret key ### Examples with Realistic Arguments #### User Pool Id and Client ID Only Used when you only need information about the user pool (ex. list users in the user pool) ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id') ``` #### Username Used when the user has not logged in yet. Start with these arguments when you plan to authenticate with either SRP (authenticate) or admin_authenticate (admin_initiate_auth). ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', username='bob') ``` #### Tokens Used after the user has already authenticated and you need to build a new Cognito instance (ex. for use in a view). ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', id_token='your-id-token', refresh_token='your-refresh-token', access_token='your-access-token') u.verify_tokens() # See method doc below; may throw an exception ``` ## Cognito Attributes After any authentication or other explicit verification of tokens, the following additional attributes will be available: - `id_claims` — A dict of verified claims from the id token - `access_claims` — A dict of verified claims from the access token ## Cognito Methods #### Register Register a user to the user pool **Important:** The arguments for `set_base_attributes` and `add_custom_attributes` methods depend on your user pool's configuration, and make sure the client id (app id) used has write permissions for the attributes you are trying to create. Example, if you want to create a user with a given_name equal to Johnson make sure the client_id you're using has permissions to edit or create given_name for a user in the pool. ```python from pycognito import Cognito u = Cognito('your-user-pool-id', 'your-client-id') u.set_base_attributes(email='you@you.com', some_random_attr='random value') u.register('username', 'password') ``` Register with custom attributes. Firstly, add custom attributes on 'General settings -> Attributes' page. Secondly, set permissions on 'Generals settings-> App clients-> Show details-> Set attribute read and write permissions' page. ```python from pycognito import Cognito u = Cognito('your-user-pool-id', 'your-client-id') u.set_base_attributes(email='you@you.com', some_random_attr='random value') u.add_custom_attributes(state='virginia', city='Centreville') u.register('username', 'password') ``` ##### Arguments - **username:** User Pool username - **password:** User Pool password - **attr_map:** Attribute map to Cognito's attributes #### Authenticate Authenticates a user If this method call succeeds the instance will have the following attributes **id_token**, **refresh_token**, **access_token**, **expires_in**, **expires_datetime**, and **token_type**. ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', username='bob') u.authenticate(password='bobs-password') ``` ##### Arguments - **password:** - User's password #### Admin Authenticate Authenticate the user using admin super privileges ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', username='bob') u.admin_authenticate(password='bobs-password') ``` - **password:** User's password #### Initiate Forgot Password Sends a verification code to the user to use to change their password. ```python u = Cognito('your-user-pool-id','your-client-id', username='bob') u.initiate_forgot_password() ``` ##### Arguments No arguments #### Confirm Forgot Password Allows a user to enter a code provided when they reset their password to update their password. ```python u = Cognito('your-user-pool-id','your-client-id', username='bob') u.confirm_forgot_password('your-confirmation-code','your-new-password') ``` ##### Arguments - **confirmation_code:** The confirmation code sent by a user's request to retrieve a forgotten password - **password:** New password #### Change Password Changes the user's password ```python from pycognito import Cognito #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.change_password('previous-password','proposed-password') ``` ##### Arguments - **previous_password:** - User's previous password - **proposed_password:** - The password that the user wants to change to. #### Confirm Sign Up Use the confirmation code that is sent via email or text to confirm the user's account ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id') u.confirm_sign_up('users-conf-code',username='bob') ``` ##### Arguments - **confirmation_code:** Confirmation code sent via text or email - **username:** User's username #### Update Profile Update the user's profile ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.update_profile({'given_name':'Edward','family_name':'Smith',},attr_map=dict()) ``` ##### Arguments - **attrs:** Dictionary of attribute name, values - **attr_map:** Dictionary map from Cognito attributes to attribute names we would like to show to our users #### Send Verification Send verification email or text for either the email or phone attributes. ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.send_verification(attribute='email') ``` ##### Arguments - **attribute:** - The attribute (email or phone) that needs to be verified #### Get User Object Returns an instance of the specified user_class. ```python u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.get_user_obj(username='bjones', attribute_list=[{'Name': 'string','Value': 'string'},], metadata={}, attr_map={"given_name":"first_name","family_name":"last_name"} ) ``` ##### Arguments - **username:** Username of the user - **attribute_list:** List of tuples that represent the user's attributes as returned by the admin_get_user or get_user boto3 methods - **metadata: (optional)** Metadata about the user - **attr_map: (optional)** Dictionary that maps the Cognito attribute names to what we'd like to display to the users #### Get User Get all of the user's attributes. Gets the user's attributes using Boto3 and uses that info to create an instance of the user_class ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id', username='bob') user = u.get_user(attr_map={"given_name":"first_name","family_name":"last_name"}) ``` ##### Arguments - **attr_map:** Dictionary map from Cognito attributes to attribute names we would like to show to our users #### Get Users Get a list of the user in the user pool. ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id') user = u.get_users(attr_map={"given_name":"first_name","family_name":"last_name"}) ``` ##### Arguments - **attr_map:** Dictionary map from Cognito attributes to attribute names we would like to show to our users #### Get Group object Returns an instance of the specified group_class. ```python u = Cognito('your-user-pool-id', 'your-client-id') group_data = {'GroupName': 'user_group', 'Description': 'description', 'Precedence': 1} group_obj = u.get_group_obj(group_data) ``` ##### Arguments - **group_data:** Dictionary with group's attributes. #### Get Group Get all of the group's attributes. Returns an instance of the group_class. Requires developer credentials. ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id') group = u.get_group(group_name='some_group_name') ``` ##### Arguments - **group_name:** Name of a group #### Get Groups Get a list of groups in the user pool. Requires developer credentials. ```python from pycognito import Cognito u = Cognito('your-user-pool-id','your-client-id') groups = u.get_groups() ``` #### Check Token Checks the exp attribute of the access_token and either refreshes the tokens by calling the renew_access_tokens method or does nothing. **IMPORTANT:** Access token is required ```python u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.check_token() ``` ##### Arguments No arguments for check_token #### Verify Tokens Verifies the current `id_token` and `access_token`. An exception will be thrown if they do not pass verification. It can be useful to call this method immediately after instantiation when you're providing externally-remembered tokens to the `Cognito()` constructor. Note that if you're calling `check_tokens()` after instantitation, you'll still want to call `verify_tokens()` afterwards it in case it did nothing. This method also ensures that the `id_claims` and `access_claims` attributes are set with the verified claims from each token. ```python u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.check_tokens() # Optional, if you want to maybe renew the tokens u.verify_tokens() ``` ##### Arguments No arguments for verify_tokens #### Logout Logs the user out of all clients and removes the expires_in, expires_datetime, id_token, refresh_token, access_token, and token_type attributes. ```python from pycognito import Cognito #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') u.logout() ``` ##### Arguments No arguments for logout #### Associate Software Token Get the secret code to issue the software token MFA code. Begins setup of time-based one-time password (TOTP) multi-factor authentication (MFA) for a user. ```python from pycognito import Cognito #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') secret_code = u.associate_software_token() # Display the secret_code to the user and enter it into a TOTP generator (such as Google Authenticator) to have them generate a 6-digit code. ``` ##### Arguments No arguments for associate_software_token #### Verify Software Token Verify the 6-digit code issued based on the secret code issued by associate_software_token. If this validation is successful, Cognito will enable Software token MFA. ```python from pycognito import Cognito #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') secret_code = u.associate_software_token() # Display the secret_code to the user and enter it into a TOTP generator (such as Google Authenticator) to have them generate a 6-digit code. code = input('Enter the 6-digit code.') device_name = input('Enter the device name') u.verify_software_token(code, device_name) ``` ##### Arguments - **code:** 6-digit code generated by the TOTP generator app - **device_name:** Name of a device #### Set User MFA Preference Enable and prioritize Software Token MFA and SMS MFA. If both Software Token MFA and SMS MFA are invalid, the preference value will be ignored. ```python from pycognito import Cognito #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', id_token='id-token',refresh_token='refresh-token', access_token='access-token') # SMS MFA are valid. SMS preference. u.set_user_mfa_preference(True, False, "SMS") # Software Token MFA are valid. Software token preference. u.set_user_mfa_preference(False, True, "SOFTWARE_TOKEN") # Both Software Token MFA and SMS MFA are valid. Software token preference u.set_user_mfa_preference(True, True, "SOFTWARE_TOKEN") # Both Software Token MFA and SMS MFA are disabled. u.set_user_mfa_preference(False, False) ``` ##### Arguments - **sms_mfa:** SMS MFA enabled / disabled (bool) - **software_token_mfa:** Software Token MFA enabled / disabled (bool) - **preferred:** Which is the priority, SMS or Software Token? The expected value is "SMS" or "SOFTWARE_TOKEN". However, it is not needed only if both of the previous arguments are False. #### Respond to Software Token MFA challenge Responds when a Software Token MFA challenge is requested at login. ```python from pycognito import Cognito from pycognito.exceptions import SoftwareTokenMFAChallengeException #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', username='bob') try: u.authenticate(password='bobs-password') except SoftwareTokenMFAChallengeException as error: code = input('Enter the 6-digit code generated by the TOTP generator (such as Google Authenticator).') u.respond_to_software_token_mfa_challenge(code) ``` When recreating a Cognito instance ```python from pycognito import Cognito from pycognito.exceptions import SoftwareTokenMFAChallengeException #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', username='bob') try: u.authenticate(password='bobs-password') except SoftwareTokenMFAChallengeException as error: mfa_tokens = error.get_tokens() u = Cognito('your-user-pool-id','your-client-id', username='bob') code = input('Enter the 6-digit code generated by the TOTP generator (such as Google Authenticator).') u.respond_to_software_token_mfa_challenge(code, mfa_tokens) ``` ##### Arguments - **code:** 6-digit code generated by the TOTP generator app - **mfa_tokens:** mfa_token stored in MFAChallengeException. Not required if you have not regenerated the Cognito instance. #### Respond to SMS MFA challenge Responds when a SMS MFA challenge is requested at login. ```python from pycognito import Cognito from pycognito.exceptions import SMSMFAChallengeException #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', username='bob') try: u.authenticate(password='bobs-password') except SMSMFAChallengeException as error: code = input('Enter the 6-digit code you received by SMS.') u.respond_to_sms_mfa_challenge(code) ``` When recreating a Cognito instance ```python from pycognito import Cognito from pycognito.exceptions import SMSMFAChallengeException #If you don't use your tokens then you will need to #use your username and password and call the authenticate method u = Cognito('your-user-pool-id','your-client-id', username='bob') try: u.authenticate(password='bobs-password') except SMSMFAChallengeException as error: mfa_tokens = error.get_tokens() u = Cognito('your-user-pool-id','your-client-id', username='bob') code = input('Enter the 6-digit code generated by the TOTP generator (such as Google Authenticator).') u.respond_to_sms_mfa_challenge(code, mfa_tokens) ``` ##### Arguments - **code:** 6-digit code you received by SMS - **mfa_tokens:** mfa_token stored in MFAChallengeException. Not required if you have not regenerated the Cognito instance. ## Cognito SRP Utility The `AWSSRP` class is used to perform [SRP(Secure Remote Password protocol)](https://www.ietf.org/rfc/rfc2945.txt) authentication. This is the preferred method of user authentication with AWS Cognito. The process involves a series of authentication challenges and responses, which if successful, results in a final response that contains ID, access and refresh tokens. ### Using AWSSRP The `AWSSRP` class takes a username, password, cognito user pool id, cognito app id, an optional client secret (if app client is configured with client secret), an optional pool_region or `boto3` client. Afterwards, the `authenticate_user` class method is used for SRP authentication. ```python import boto3 from pycognito.aws_srp import AWSSRP client = boto3.client('cognito-idp') aws = AWSSRP(username='username', password='password', pool_id='user_pool_id', client_id='client_id', client=client) tokens = aws.authenticate_user() ``` ## Device Authentication Support You must use the `USER_SRP_AUTH` authentication flow to use the device tracking feature. Read more about [Remembered Devices](https://repost.aws/knowledge-center/cognito-user-pool-remembered-devices) ### Receiving DeviceKey and DeviceGroupKey Once the `authenticate_user` class method is used for SRP authentication, the response also returns `DeviceKey` and `DeviceGrouKey`. These Keys will later be used to confirm the device. ```python import boto3 from pycognito.aws_srp import AWSSRP client = boto3.client('cognito-idp') aws = AWSSRP(username='username', password='password', pool_id='user_pool_id', client_id='client_id', client=client) tokens = aws.authenticate_user() device_key = tokens["AuthenticationResult"]["NewDeviceMetadata"]["DeviceKey"] device_group_key = tokens["AuthenticationResult"]["NewDeviceMetadata"]["DeviceGroupKey"] ``` ### Confirming a Device The `confirm_device` class method is used for confirming a device, it takes two inputs, `tokens` and `DeviceName` (`DeviceName` is optional). The method returns two values, `response` and `device_password`. `device_password` will later be used to authenticate your device with the Cognito user pool. ```python response, device_password = user.confirm_device(tokens=tokens) ``` ### Updating Device Status The `update_device_status` class method is used to update whether or not your device should be remembered. This method takes three inputs, `is_remembered`, `access_token` and `device_key`. `is_remembered` is a boolean value, which sets the device status as `"remembered"` on `True` and `"not_remembered"` on `False`, `access_token` is the Access Token provided by Cognito and `device_key` is the key provided by the `authenticate_user` method. ```python response = user.update_device_status(False, tokens["AuthenticationResult"]["AccessToken"], device_key) ``` ### Authenticating your Device To authenticate your Device, you can just add `device_key`, `device_group_key` and `device_password` to the AWSSRP class. ```python import boto3 from pycognito.aws_srp import AWSSRP client = boto3.client('cognito-idp') aws = AWSSRP(username='username', password='password', pool_id='user_pool_id', client_id='client_id', client=client, device_key="device_key", device_group_key="device_group_key", device_password="device_password") tokens = aws.authenticate_user() ``` ### Forget Device To forget device, you can call the `forget_device` class method. It takes `access_token` and `device_key` as input. ```python resonse = aws.forget_device(access_token='access_token', device_key='device_key') ``` ## SRP Requests Authenticator `pycognito.utils.RequestsSrpAuth` is a [Requests](https://docs.python-requests.org/en/latest/) authentication plugin to automatically populate an HTTP header with a Cognito token. By default, it'll populate the `Authorization` header using the Cognito Access Token as a `bearer` token. `RequestsSrpAuth` handles fetching new tokens using the refresh tokens. ### Usage ```python import requests from pycognito.utils import RequestsSrpAuth auth = RequestsSrpAuth( username='myusername', password='secret', user_pool_id='eu-west-1_1234567', client_id='4dn6jbcbhqcofxyczo3ms9z4cc', user_pool_region='eu-west-1', ) response = requests.get('http://test.com', auth=auth) ``` NabuCasa-pycognito-4f6c90a/pycognito/000077500000000000000000000000001462135417600176425ustar00rootroot00000000000000NabuCasa-pycognito-4f6c90a/pycognito/__init__.py000066400000000000000000001124261462135417600217610ustar00rootroot00000000000000import ast import base64 import datetime import re import boto3 from envs import env import jwt import requests from .aws_srp import AWSSRP from .exceptions import TokenVerificationException, MFAChallengeException def cognito_to_dict(attr_list, attr_map=None): if attr_map is None: attr_map = {} attr_dict = {} for attr in attr_list: name = attr.get("Name") value = attr.get("Value") if value in ["true", "false"]: value = ast.literal_eval(value.capitalize()) name = attr_map.get(name, name) attr_dict[name] = value return attr_dict def dict_to_cognito(attributes, attr_map=None): """ :param attributes: Dictionary of User Pool attribute names/values :return: list of User Pool attribute formatted dicts: {'Name': , 'Value': } """ if attr_map is None: attr_map = {} for key, value in attr_map.items(): if value in attributes.keys(): attributes[key] = attributes.pop(value) def normalize(val): if isinstance(val, bool): return "true" if val else "false" return val return [ {"Name": key, "Value": normalize(value)} for key, value in attributes.items() ] def camel_to_snake(camel_str): """ :param camel_str: string :return: string converted from a CamelCase to a snake_case """ return re.sub( "([a-z0-9])([A-Z])", r"\1_\2", re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_str) ).lower() def snake_to_camel(snake_str): """ :param snake_str: string :return: string converted from a snake_case to a CamelCase """ components = snake_str.split("_") return "".join(x.title() for x in components) class UserObj: def __init__( self, username, attribute_list, cognito_obj, metadata=None, attr_map=None ): """ :param username: :param attribute_list: :param metadata: Dictionary of User metadata """ self.username = username self._cognito = cognito_obj self._attr_map = {} if attr_map is None else attr_map self._data = cognito_to_dict(attribute_list, self._attr_map) self.sub = self._data.pop("sub", None) self.email_verified = self._data.pop("email_verified", None) self.phone_number_verified = self._data.pop("phone_number_verified", None) self._metadata = {} if metadata is None else metadata def __repr__(self): return f"<{self.__class__.__name__}: {self.__unicode__()}>" def __unicode__(self): return self.username def __getattr__(self, name): if name in list(self.__dict__.get("_data", {}).keys()): return self._data.get(name) if name in list(self.__dict__.get("_metadata", {}).keys()): return self._metadata.get(name) raise AttributeError(name) def __setattr__(self, name, value): if name in list(self.__dict__.get("_data", {}).keys()): self._data[name] = value else: super().__setattr__(name, value) def save(self, admin=False): if admin: self._cognito.admin_update_profile(self._data, self._attr_map) return self._cognito.update_profile(self._data, self._attr_map) def delete(self, admin=False): if admin: self._cognito.admin_delete_user() return self._cognito.delete_user() class GroupObj: def __init__(self, group_data, cognito_obj): """ :param group_data: a dictionary with information about a group :param cognito_obj: an instance of the Cognito class """ self._data = group_data self._cognito = cognito_obj self.group_name = self._data.pop("GroupName", None) self.description = self._data.pop("Description", None) self.creation_date = self._data.pop("CreationDate", None) self.last_modified_date = self._data.pop("LastModifiedDate", None) self.role_arn = self._data.pop("RoleArn", None) self.precedence = self._data.pop("Precedence", None) def __unicode__(self): return self.group_name def __repr__(self): return f"<{self.__class__.__name__}: {self.__unicode__()}>" class Cognito: user_class = UserObj group_class = GroupObj def __init__( self, user_pool_id, client_id, user_pool_region=None, username=None, id_token=None, refresh_token=None, access_token=None, client_secret=None, access_key=None, secret_key=None, session=None, botocore_config=None, boto3_client_kwargs=None, ): """ :param user_pool_id: Cognito User Pool ID :param client_id: Cognito User Pool Application client ID :param username: User Pool username :param id_token: ID Token returned by authentication :param refresh_token: Refresh Token returned by authentication :param access_token: Access Token returned by authentication :param access_key: AWS IAM access key :param secret_key: AWS IAM secret key :param session: Boto3 client session :param botocore_config: Botocore Config object for the client :param boto3_client_kwargs: Keyword args to pass to Boto3 for client creation """ self.user_pool_id = user_pool_id self.client_id = client_id self.user_pool_region = ( user_pool_region if user_pool_region else self.user_pool_id.split("_")[0] ) self.username = username self.id_token = id_token self.access_token = access_token self.refresh_token = refresh_token self.client_secret = client_secret self.token_type = None self.id_claims = None self.access_claims = None self.custom_attributes = None self.base_attributes = None self.pool_jwk = None self.mfa_tokens = None if not boto3_client_kwargs: boto3_client_kwargs = {} if access_key and secret_key: boto3_client_kwargs["aws_access_key_id"] = access_key boto3_client_kwargs["aws_secret_access_key"] = secret_key self.pool_domain_url = boto3_client_kwargs.get("endpoint_url", None) if self.user_pool_region: boto3_client_kwargs["region_name"] = self.user_pool_region if botocore_config: boto3_client_kwargs["config"] = botocore_config if session: self.client = session.client("cognito-idp", **boto3_client_kwargs) else: self.client = boto3.client("cognito-idp", **boto3_client_kwargs) @property def user_pool_url(self): if self.pool_domain_url: return f"{self.pool_domain_url}/{self.user_pool_id}" return f"https://cognito-idp.{self.user_pool_region}.amazonaws.com/{self.user_pool_id}" def get_keys(self): if self.pool_jwk: return self.pool_jwk # Check for the dictionary in environment variables. pool_jwk_env = env("COGNITO_JWKS", {}, var_type="dict") if pool_jwk_env: self.pool_jwk = pool_jwk_env # If it is not there use the requests library to get it else: self.pool_jwk = requests.get( f"{self.user_pool_url}/.well-known/jwks.json", timeout=15 ).json() return self.pool_jwk def get_key(self, kid): keys = self.get_keys().get("keys") key = list(filter(lambda x: x.get("kid") == kid, keys)) return key[0] def verify_tokens(self): """ Verify the current id_token and access_token. An exception will be thrown if they do not pass verification. It can be useful to call this method after creating a Cognito instance where you've provided externally-remembered token values. """ self.verify_token(self.id_token, "id_token", "id") self.verify_token(self.access_token, "access_token", "access") def verify_token(self, token, id_name, token_use): # https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html kid = jwt.get_unverified_header(token).get("kid") hmac_key = jwt.api_jwk.PyJWK(self.get_key(kid)).key required_claims = (["aud"] if token_use != "access" else []) + ["iss", "exp"] try: decoded = jwt.api_jwt.decode_complete( token, hmac_key, algorithms=["RS256"], audience=self.client_id if token_use != "access" else None, issuer=self.user_pool_url, options={ "require": required_claims, "verify_iat": False, }, ) except jwt.PyJWTError as err: raise TokenVerificationException( f"Your {id_name!r} token could not be verified ({err})." ) from None verified, header = decoded["payload"], decoded["header"] token_use_verified = verified.get("token_use") == token_use if not token_use_verified: raise TokenVerificationException( f"Your {id_name!r} token use ({token_use!r}) could not be verified." ) if (iat := verified.get("iat")) is not None: try: int(iat) except ValueError as execption: raise TokenVerificationException( f"Your {id_name!r} token's iat claim is not a valid integer." ) from execption # Compute and verify at_hash (formerly done by python-jose) if "at_hash" in verified: alg_obj = jwt.get_algorithm_by_name(header["alg"]) digest = alg_obj.compute_hash_digest(self.access_token) at_hash = base64.urlsafe_b64encode(digest[: (len(digest) // 2)]).rstrip("=") if at_hash != verified["at_hash"]: raise TokenVerificationException( "at_hash claim does not match access_token." ) setattr(self, id_name, token) setattr(self, f"{token_use}_claims", verified) return verified def get_user_obj( self, username=None, attribute_list=None, metadata=None, attr_map=None ): """ Returns the specified user :param username: Username of the user :param attribute_list: List of tuples that represent the user's attributes as returned by the admin_get_user or get_user boto3 methods :param metadata: Metadata about the user :param attr_map: Dictionary that maps the Cognito attribute names to what we'd like to display to the users :return: dictionary of the Cognito user response """ return self.user_class( username=username, attribute_list=attribute_list, cognito_obj=self, metadata=metadata, attr_map=attr_map, ) def get_group_obj(self, group_data): """ Instantiates the self.group_class :param group_data: a dictionary with information about a group :return: an instance of the self.group_class """ return self.group_class(group_data=group_data, cognito_obj=self) def switch_session(self, session): """ Primarily used for unit testing so we can take advantage of the placebo library (https://githhub.com/garnaat/placebo) :param session: boto3 session :return: """ self.client = session.client("cognito-idp") def check_token(self, renew=True): """ Checks the exp attribute of the access_token and either refreshes the tokens by calling the renew_access_tokens method or does nothing :param renew: bool indicating whether to refresh on expiration :return: bool indicating whether access_token has expired """ if not self.access_token: raise AttributeError("Access Token Required to Check Token") now = datetime.datetime.now() dec_access_token = jwt.decode( self.access_token, options={"verify_signature": False} ) if now > datetime.datetime.fromtimestamp(dec_access_token["exp"]): expired = True if renew: self.renew_access_token() else: expired = False return expired def set_base_attributes(self, **kwargs): self.base_attributes = kwargs def add_custom_attributes(self, **kwargs): custom_key = "custom" custom_attributes = {} for old_key, value in kwargs.items(): new_key = custom_key + ":" + old_key custom_attributes[new_key] = value self.custom_attributes = custom_attributes def register(self, username, password, attr_map=None, client_metadata=None): """ Register the user. Other base attributes from AWS Cognito User Pools are address, birthdate, email, family_name (last name), gender, given_name (first name), locale, middle_name, name, nickname, phone_number, picture, preferred_username, profile, zoneinfo, updated at, website :param username: User Pool username :param password: User Pool password :param attr_map: Attribute map to Cognito's attributes :param client_metadata: Metadata about the user that will be used for ClientMetadata :return response: Response from Cognito Example response:: { 'UserConfirmed': True|False, 'CodeDeliveryDetails': { 'Destination': 'string', # This value will be obfuscated 'DeliveryMedium': 'SMS'|'EMAIL', 'AttributeName': 'string' } } """ if self.base_attributes is None: attributes = {} else: attributes = self.base_attributes.copy() if self.custom_attributes: attributes.update(self.custom_attributes) cognito_attributes = dict_to_cognito(attributes, attr_map) params = { "ClientId": self.client_id, "Username": username, "Password": password, "UserAttributes": cognito_attributes, } if client_metadata is not None: params["ClientMetadata"] = client_metadata self._add_secret_hash(params, "SecretHash") response = self.client.sign_up(**params) attributes.update(username=username, password=password) self._set_attributes(response, attributes) response.pop("ResponseMetadata") return response def admin_confirm_sign_up(self, username=None): """ Confirms user registration as an admin without using a confirmation code. Works on any user. :param username: User's username :return: """ if not username: username = self.username self.client.admin_confirm_sign_up( UserPoolId=self.user_pool_id, Username=username, ) def confirm_sign_up(self, confirmation_code, username=None): """ Using the confirmation code that is either sent via email or text message. :param confirmation_code: Confirmation code sent via text or email :param username: User's username :return: """ if not username: username = self.username params = { "ClientId": self.client_id, "Username": username, "ConfirmationCode": confirmation_code, } self._add_secret_hash(params, "SecretHash") self.client.confirm_sign_up(**params) def resend_confirmation_code(self, username): """ Trigger resending the confirmation code message. :param username: User's username :return: """ params = { "ClientId": self.client_id, "Username": username, } self._add_secret_hash(params, "SecretHash") self.client.resend_confirmation_code(**params) def admin_authenticate(self, password): """ Authenticate the user using admin super privileges :param password: User's password :return: """ auth_params = {"USERNAME": self.username, "PASSWORD": password} self._add_secret_hash(auth_params, "SECRET_HASH") tokens = self.client.admin_initiate_auth( UserPoolId=self.user_pool_id, ClientId=self.client_id, # AuthFlow='USER_SRP_AUTH'|'REFRESH_TOKEN_AUTH'|'REFRESH_TOKEN'|'CUSTOM_AUTH'|'ADMIN_NO_SRP_AUTH', AuthFlow="ADMIN_NO_SRP_AUTH", AuthParameters=auth_params, ) self._set_tokens(tokens) def authenticate(self, password, client_metadata=None): """ Authenticate the user using the SRP protocol :param password: The user's passsword :param client_metadata: Metadata you can provide for custom workflows that RespondToAuthChallenge triggers. :return: """ aws = AWSSRP( username=self.username, password=password, pool_id=self.user_pool_id, client_id=self.client_id, client=self.client, client_secret=self.client_secret, ) try: tokens = aws.authenticate_user(client_metadata=client_metadata) except MFAChallengeException as mfa_challenge: self.mfa_tokens = mfa_challenge.get_tokens() raise mfa_challenge self._set_tokens(tokens) def new_password_challenge(self, password, new_password): """ Respond to the new password challenge using the SRP protocol :param password: The user's current passsword :param password: The user's new passsword """ aws = AWSSRP( username=self.username, password=password, pool_id=self.user_pool_id, client_id=self.client_id, client=self.client, client_secret=self.client_secret, ) tokens = aws.set_new_password_challenge(new_password) self._set_tokens(tokens) def logout(self): """ Logs the user out of all clients and removes the expires_in, expires_datetime, id_token, refresh_token, access_token, and token_type attributes :return: """ self.client.global_sign_out(AccessToken=self.access_token) self.id_token = None self.refresh_token = None self.access_token = None self.token_type = None def admin_update_profile(self, attrs, attr_map=None): user_attrs = dict_to_cognito(attrs, attr_map) self.client.admin_update_user_attributes( UserPoolId=self.user_pool_id, Username=self.username, UserAttributes=user_attrs, ) def update_profile(self, attrs, attr_map=None): """ Updates User attributes :param attrs: Dictionary of attribute name, values :param attr_map: Dictionary map from Cognito attributes to attribute names we would like to show to our users """ user_attrs = dict_to_cognito(attrs, attr_map) self.client.update_user_attributes( UserAttributes=user_attrs, AccessToken=self.access_token ) def get_user(self, attr_map=None): """ Returns a UserObj (or whatever the self.user_class is) by using the user's access token. :param attr_map: Dictionary map from Cognito attributes to attribute names we would like to show to our users :return: UserObj dictionary """ user = self.client.get_user(AccessToken=self.access_token) user_metadata = { "username": user.get("Username"), "id_token": self.id_token, "access_token": self.access_token, "refresh_token": self.refresh_token, } return self.get_user_obj( username=self.username, attribute_list=user.get("UserAttributes"), metadata=user_metadata, attr_map=attr_map, ) def get_users(self, attr_map=None): """ Returns all users for a user pool. Returns instances of the self.user_class. :param attr_map: Dictionary map from Cognito attributes to attribute names we would like to show to our users :return: list of self.user_class """ response = self.client.list_users(UserPoolId=self.user_pool_id) user_list = response.get("Users") page_token = response.get("PaginationToken") while page_token: response = self.client.list_users( UserPoolId=self.user_pool_id, PaginationToken=page_token ) user_list.extend(response.get("Users")) page_token = response.get("PaginationToken") return [ self.get_user_obj( user.get("Username"), attribute_list=user.get("Attributes"), metadata={"username": user.get("Username")}, attr_map=attr_map, ) for user in user_list ] def admin_get_user(self, attr_map=None): """ Get the user's details using admin super privileges. :param attr_map: Dictionary map from Cognito attributes to attribute names we would like to show to our users :return: UserObj dictionary """ user = self.client.admin_get_user( UserPoolId=self.user_pool_id, Username=self.username ) user_metadata = { "enabled": user.get("Enabled"), "user_status": user.get("UserStatus"), "username": user.get("Username"), "create_date": user.get("UserCreateDate"), "modified_date": user.get("UserLastModifiedDate"), "id_token": self.id_token, "access_token": self.access_token, "refresh_token": self.refresh_token, } return self.get_user_obj( username=self.username, attribute_list=user.get("UserAttributes"), metadata=user_metadata, attr_map=attr_map, ) def admin_create_user( self, username, temporary_password="", additional_kwargs=None, attr_map=None, **kwargs, ): """ Create a user using admin super privileges. :param username: User Pool username :param temporary_password: The temporary password to give the user. Pass None or omit this to make Cognito generate a temporary password for the user. :param additional_kwargs: Dictionary with request params, such as MessageAction. :param attr_map: Attribute map to Cognito's attributes :param kwargs: Additional User Pool attributes :return response: Response from Cognito """ if additional_kwargs is None: additional_kwargs = {} if temporary_password: additional_kwargs["TemporaryPassword"] = temporary_password response = self.client.admin_create_user( UserPoolId=self.user_pool_id, Username=username, UserAttributes=dict_to_cognito(kwargs, attr_map), **additional_kwargs, ) kwargs.update(username=username) self._set_attributes(response, kwargs) response.pop("ResponseMetadata") return response def send_verification(self, attribute="email"): """ Sends the user an attribute verification code for the specified attribute name. :param attribute: Attribute to confirm. Defaults to "email" """ self.check_token() self.client.get_user_attribute_verification_code( AccessToken=self.access_token, AttributeName=attribute ) def validate_verification(self, confirmation_code, attribute="email"): """ Verifies the specified user attributes in the user pool. :param confirmation_code: Code sent to user upon intiating verification :param attribute: Attribute to confirm. Defaults to "email" """ self.check_token() return self.client.verify_user_attribute( AccessToken=self.access_token, AttributeName=attribute, Code=confirmation_code, ) def renew_access_token(self): """ Sets a new access token on the User using the cached refresh token. """ auth_params = {"REFRESH_TOKEN": self.refresh_token} self._add_secret_hash(auth_params, "SECRET_HASH") refresh_response = self.client.initiate_auth( ClientId=self.client_id, AuthFlow="REFRESH_TOKEN_AUTH", AuthParameters=auth_params, ) self._set_tokens(refresh_response) def initiate_forgot_password(self): """ Sends a verification code to the user to use to change their password. """ params = {"ClientId": self.client_id, "Username": self.username} self._add_secret_hash(params, "SecretHash") self.client.forgot_password(**params) def delete_user(self): self.client.delete_user(AccessToken=self.access_token) def admin_delete_user(self): self.client.admin_delete_user( UserPoolId=self.user_pool_id, Username=self.username ) def admin_reset_password(self, username, client_metadata=None): if client_metadata is None: client_metadata = {} self.client.admin_reset_user_password( UserPoolId=self.user_pool_id, Username=username, ClientMetadata=client_metadata, ) def confirm_forgot_password(self, confirmation_code, password): """ Allows a user to provide their verification code and choose a new password. :param confirmation_code: The confirmation code sent by a user's request to retrieve a forgotten password :param password: New password """ params = { "ClientId": self.client_id, "Username": self.username, "ConfirmationCode": confirmation_code, "Password": password, } self._add_secret_hash(params, "SecretHash") response = self.client.confirm_forgot_password(**params) self._set_attributes(response, {"password": password}) def change_password(self, previous_password, proposed_password): """ Change the User password """ self.check_token() response = self.client.change_password( PreviousPassword=previous_password, ProposedPassword=proposed_password, AccessToken=self.access_token, ) self._set_attributes(response, {"password": proposed_password}) def _add_secret_hash(self, parameters, key): """ Helper function that computes SecretHash and adds it to a parameters dictionary at a specified key """ if self.client_secret is not None: secret_hash = AWSSRP.get_secret_hash( self.username, self.client_id, self.client_secret ) parameters[key] = secret_hash def _set_tokens(self, tokens): """ Helper function to verify and set token attributes based on a Cognito AuthenticationResult. """ self.verify_token( tokens["AuthenticationResult"]["AccessToken"], "access_token", "access" ) self.verify_token(tokens["AuthenticationResult"]["IdToken"], "id_token", "id") if "RefreshToken" in tokens["AuthenticationResult"]: self.refresh_token = tokens["AuthenticationResult"]["RefreshToken"] self.token_type = tokens["AuthenticationResult"]["TokenType"] def _set_attributes(self, response, attribute_dict): """ Set user attributes based on response code :param response: HTTP response from Cognito :param attribute_dict: Dictionary of attribute names and values """ status_code = response.get( "HTTPStatusCode", response["ResponseMetadata"]["HTTPStatusCode"] ) if status_code == 200: for key, value in attribute_dict.items(): setattr(self, key, value) def get_group(self, group_name): """ Get a group by name :param group_name: name of a group :return: instance of the self.group_class """ response = self.client.get_group( GroupName=group_name, UserPoolId=self.user_pool_id ) return self.get_group_obj(response.get("Group")) def get_groups(self): """ Returns all groups for a user pool. :return: list of instances of self.group_class """ response = self.client.list_groups(UserPoolId=self.user_pool_id) return [self.get_group_obj(group_data) for group_data in response.get("Groups")] def admin_add_user_to_group(self, username, group_name): """ Add the user to the specified group :param username: the username :param group_name: the name of the group to add the user to :return: """ self.client.admin_add_user_to_group( UserPoolId=self.user_pool_id, Username=username, GroupName=group_name, ) def admin_remove_user_from_group(self, username, group_name): """ Remove the user from the specified group :param username: the username :param group_name: the name of the group to remove the user from :return: """ self.client.admin_remove_user_from_group( UserPoolId=self.user_pool_id, Username=username, GroupName=group_name, ) def admin_list_groups_for_user(self, username): """ Get the list of groups a user belongs to :param username: :return: List of group objects """ def process_groups_response(groups_response): groups = [] for group_dict in groups_response["Groups"]: groups.append(group_dict["GroupName"]) return groups groups_response = self.client.admin_list_groups_for_user( Username=username, UserPoolId=self.user_pool_id, Limit=60 ) user_groups = process_groups_response(groups_response) while "NextToken" in groups_response.keys(): groups_response = self.client.admin_list_groups_for_user( Username=username, UserPoolId=self.user_pool_id, Limit=60, NextToken=groups_response["NextToken"], ) new_groups = process_groups_response(groups_response) user_groups.extend(new_groups) return user_groups def admin_enable_user(self, username): """ Enable a user :param username: :return: """ self.client.admin_enable_user( UserPoolId=self.user_pool_id, Username=username, ) def admin_disable_user(self, username): """ Disable a user :param username: :return: """ self.client.admin_disable_user( UserPoolId=self.user_pool_id, Username=username, ) def admin_create_identity_provider( self, pool_id, provider_name, provider_type, provider_details, **kwargs ): """ Creates an identity provider :param pool_id: The user pool ID :param provider_name: The identity provider name :param provider_type: The identity provider type :param provider_details: The identity provider details :return: """ self.client.create_identity_provider( UserPoolId=pool_id, ProviderName=provider_name, ProviderType=provider_type, ProviderDetails=provider_details, **kwargs, ) def admin_describe_identity_provider(self, pool_id, provider_name): """ Describes an existing identity provider :param pool_id: The user pool ID :param provider_name: The identity provider name :return: dict of identity provider """ return self.client.describe_identity_provider( UserPoolId=pool_id, ProviderName=provider_name ) def admin_update_identity_provider(self, pool_id, provider_name, **kwargs): """ Updates an existing identity provider :param pool_id: The user pool ID :param provider_name: The identity provider name :return: """ self.client.update_identity_provider( UserPoolId=pool_id, ProviderName=provider_name, **kwargs, ) def describe_user_pool_client(self, pool_id: str, client_id: str): """ Returns configuration information of a specified user pool app client :param pool_id: The user pool ID :param client_id: The client ID :return: client json """ return self.client.describe_user_pool_client( UserPoolId=pool_id, ClientId=client_id )["UserPoolClient"] def admin_update_user_pool_client(self, pool_id: str, client_id: str, **kwargs): """ Updates configuration information of a specified user pool app client :param pool_id: The identity pool ID :param client_id: The identity pool name :return: """ self.client.update_user_pool_client( UserPoolId=pool_id, ClientId=client_id, **kwargs, ) def associate_software_token(self): """ Get the Secret code used for Software Token MFA. :return: Secret code :rtype: string """ response = self.client.associate_software_token(AccessToken=self.access_token) return response["SecretCode"] def verify_software_token(self, code, device_name=""): """ Verify the value generated by TOTP to complete the registration of Software Token MFA. :param code: The value generated by TOTP :param device_name: Device name to register (optional) :return: verify success :rtype: bool """ response = self.client.verify_software_token( AccessToken=self.access_token, UserCode=code, FriendlyDeviceName=device_name ) return response["Status"] == "SUCCESS" def set_user_mfa_preference(self, sms_mfa, software_token_mfa, preferred=None): """ Register the preference of MFA. :param sms_mfa: Enable SMS MFA. :type sms_mfa: bool :param software_token_mfa: Enable Software Token MFA. :type software_token_mfa: bool :param preferred: "SMS" or "SOFTWARE_TOKEN", or None if both of the previous arguments are False. :type preferred: string :return: """ sms_mfa_settings = {"Enabled": bool(sms_mfa), "PreferredMfa": False} software_token_mfa_settings = { "Enabled": bool(software_token_mfa), "PreferredMfa": False, } if not (bool(sms_mfa) or bool(software_token_mfa)): # Disable MFA pass elif preferred == "SMS": sms_mfa_settings["PreferredMfa"] = True elif preferred == "SOFTWARE_TOKEN": software_token_mfa_settings["PreferredMfa"] = True else: raise ValueError( "preferred must have a value of 'SMS', 'SOFTWARE_TOKEN', or None." ) self.client.set_user_mfa_preference( SMSMfaSettings=sms_mfa_settings, SoftwareTokenMfaSettings=software_token_mfa_settings, AccessToken=self.access_token, ) def respond_to_software_token_mfa_challenge(self, code, mfa_tokens=None): """ Response to software token MFA challenge. :param code: software token MFA code. :type code: string :param mfa_token: Token stored in MFAChallengeException. Optional if you have not regenerated the Cognito instance. :type mfa_token: string :return: """ if not mfa_tokens: mfa_tokens = self.mfa_tokens challenge_responses = { "USERNAME": self.username, "SOFTWARE_TOKEN_MFA_CODE": str(code), } self._add_secret_hash(challenge_responses, "SECRET_HASH") tokens = self.client.respond_to_auth_challenge( ClientId=self.client_id, Session=mfa_tokens["Session"], ChallengeName="SOFTWARE_TOKEN_MFA", ChallengeResponses=challenge_responses, ) self._set_tokens(tokens) def respond_to_sms_mfa_challenge(self, code, mfa_tokens=None): """ Response to SMS MFA challenge. :param code: SMS MFA code. :type code: string :param mfa_tokens: Token stored in MFAChallengeException. Optional if you have not regenerated the Cognito instance. :type mfa_tokens: string :return: """ if not mfa_tokens: mfa_tokens = self.mfa_tokens challenge_responses = { "USERNAME": self.username, "SMS_MFA_CODE": code, } self._add_secret_hash(challenge_responses, "SECRET_HASH") tokens = self.client.respond_to_auth_challenge( ClientId=self.client_id, Session=mfa_tokens["Session"], ChallengeName="SMS_MFA", ChallengeResponses=challenge_responses, ) self._set_tokens(tokens) NabuCasa-pycognito-4f6c90a/pycognito/aws_srp.py000066400000000000000000000515531462135417600217030ustar00rootroot00000000000000import base64 import binascii import datetime import hashlib import hmac import json import os import re import platform import requests import boto3 from .exceptions import ( ForceChangePasswordException, SoftwareTokenMFAChallengeException, SMSMFAChallengeException, ) # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22 N_HEX = ( "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" "83655D23DCA3AD961C62F356208552BB9ED529077096966D" "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF" ) # https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L49 G_HEX = "2" INFO_BITS = bytearray("Caldera Derived Key", "utf-8") WEEKDAY_NAMES = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] MONTH_NAMES = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ] def hash_sha256(buf): """AuthenticationHelper.hash""" value = hashlib.sha256(buf).hexdigest() return (64 - len(value)) * "0" + value def hex_hash(hex_string): return hash_sha256(bytearray.fromhex(hex_string)) def hex_to_long(hex_string): return int(hex_string, 16) def long_to_hex(long_num): return f"{long_num:x}" def get_random(nbytes): random_hex = binascii.hexlify(os.urandom(nbytes)) return hex_to_long(random_hex) def pad_hex(long_int): """ Converts a Long integer (or hex string) to hex format padded with zeroes for hashing :param {Long integer|String} long_int Number or string to pad. :return {String} Padded hex string. """ if not isinstance(long_int, str): hash_str = long_to_hex(long_int) else: hash_str = long_int if len(hash_str) % 2 == 1: hash_str = f"0{hash_str}" elif hash_str[0] in "89ABCDEFabcdef": hash_str = f"00{hash_str}" return hash_str def compute_hkdf(ikm, salt): """ Standard hkdf algorithm :param {Buffer} ikm Input key material. :param {Buffer} salt Salt value. :return {Buffer} Strong key material. @private """ prk = hmac.new(salt, ikm, hashlib.sha256).digest() info_bits_update = INFO_BITS + bytearray(chr(1), "utf-8") hmac_hash = hmac.new(prk, info_bits_update, hashlib.sha256).digest() return hmac_hash[:16] def calculate_u(big_a, big_b): """ Calculate the client's value U which is the hash of A and B :param {Long integer} big_a Large A value. :param {Long integer} big_b Server B value. :return {Long integer} Computed U value. """ u_hex_hash = hex_hash(pad_hex(big_a) + pad_hex(big_b)) return hex_to_long(u_hex_hash) def generate_hash_device(device_group_key, device_key): # source: https://github.com/amazon-archives/amazon-cognito-identity-js/blob/6b87f1a30a998072b4d98facb49dcaf8780d15b0/src/AuthenticationHelper.js#L137 # random device password, which will be used for DEVICE_SRP_AUTH flow device_password = base64.standard_b64encode(os.urandom(40)).decode("utf-8") combined_string = f"{device_group_key}{device_key}:{device_password}" combined_string_hash = hash_sha256(combined_string.encode("utf-8")) salt = pad_hex(get_random(16)) x_value = hex_to_long(hex_hash(salt + combined_string_hash)) g_value = hex_to_long(G_HEX) big_n = hex_to_long(N_HEX) verifier_device_not_padded = pow(g_value, x_value, big_n) verifier = pad_hex(verifier_device_not_padded) device_secret_verifier_config = { "PasswordVerifier": base64.standard_b64encode( bytearray.fromhex(verifier) ).decode("utf-8"), "Salt": base64.standard_b64encode(bytearray.fromhex(salt)).decode("utf-8"), } return device_password, device_secret_verifier_config class AWSSRP: SMS_MFA_CHALLENGE = "SMS_MFA" SOFTWARE_TOKEN_MFA_CHALLENGE = "SOFTWARE_TOKEN_MFA" NEW_PASSWORD_REQUIRED_CHALLENGE = "NEW_PASSWORD_REQUIRED" PASSWORD_VERIFIER_CHALLENGE = "PASSWORD_VERIFIER" DEVICE_SRP_CHALLENGE = "DEVICE_SRP_AUTH" DEVICE_PASSWORD_VERIFIER_CHALLENGE = "DEVICE_PASSWORD_VERIFIER" def __init__( self, username, password, pool_id, client_id, pool_region=None, client=None, client_secret=None, device_key=None, device_group_key=None, device_password=None, ): if pool_region is not None and client is not None: raise ValueError( "pool_region and client should not both be specified " "(region should be passed to the boto3 client instead)" ) if ( device_key is not None or device_group_key is not None or device_password is not None ): if ( device_key is None or device_group_key is None or device_password is None ): raise ValueError( "Either all device_key, device_group_key, and device_password should be specified or none at all " ) self.username = username self.password = password self.pool_id = pool_id self.client_id = client_id self.client_secret = client_secret self.client = ( client if client else boto3.client("cognito-idp", region_name=pool_region) ) self.device_key = device_key self.device_group_key = device_group_key self.device_password = device_password self.big_n = hex_to_long(N_HEX) self.val_g = hex_to_long(G_HEX) self.val_k = hex_to_long(hex_hash("00" + N_HEX + "0" + G_HEX)) self.small_a_value = self.generate_random_small_a() self.large_a_value = self.calculate_a() self.access_token = None self.device_name = None self.cognito_idp_url = None def generate_random_small_a(self): """ helper function to generate a random big integer :return {Long integer} a random value. """ random_long_int = get_random(128) return random_long_int % self.big_n def calculate_a(self): """ Calculate the client's public value A = g^a%N with the generated random number a :param {Long integer} a Randomly generated small A. :return {Long integer} Computed large A. """ big_a = pow(self.val_g, self.small_a_value, self.big_n) # safety check if (big_a % self.big_n) == 0: raise ValueError("Safety check for A failed") return big_a def get_password_authentication_key(self, username, password, server_b_value, salt): """ Calculates the final hkdf based on computed S value, and computed U value and the key :param {String} username Username. :param {String} password Password. :param {Long integer} server_b_value Server B value. :param {Long integer} salt Generated salt. :return {Buffer} Computed HKDF value. """ u_value = calculate_u(self.large_a_value, server_b_value) if u_value == 0: raise ValueError("U cannot be zero.") username_password = f"{self.pool_id.split('_')[1]}{username}:{password}" username_password_hash = hash_sha256(username_password.encode("utf-8")) x_value = hex_to_long(hex_hash(pad_hex(salt) + username_password_hash)) g_mod_pow_xn = pow(self.val_g, x_value, self.big_n) int_value2 = server_b_value - self.val_k * g_mod_pow_xn s_value = pow(int_value2, self.small_a_value + u_value * x_value, self.big_n) hkdf = compute_hkdf( bytearray.fromhex(pad_hex(s_value)), bytearray.fromhex(pad_hex(long_to_hex(u_value))), ) return hkdf def get_device_authentication_key( self, device_group_key, device_key, device_password, server_b_value, salt ): u_value = calculate_u(self.large_a_value, server_b_value) if u_value == 0: raise ValueError("U cannot be zero.") username_password = f"{device_group_key}{device_key}:{device_password}" username_password_hash = hash_sha256(username_password.encode("utf-8")) x_value = hex_to_long(hex_hash(pad_hex(salt) + username_password_hash)) g_mod_pow_xn = pow(self.val_g, x_value, self.big_n) int_value2 = server_b_value - self.val_k * g_mod_pow_xn s_value = pow(int_value2, self.small_a_value + u_value * x_value, self.big_n) hkdf = compute_hkdf( bytearray.fromhex(pad_hex(s_value)), bytearray.fromhex(pad_hex(long_to_hex(u_value))), ) return hkdf def get_auth_params(self): auth_params = { "USERNAME": self.username, "SRP_A": long_to_hex(self.large_a_value), } if self.client_secret is not None: auth_params.update( { "SECRET_HASH": self.get_secret_hash( self.username, self.client_id, self.client_secret ) } ) if self.device_key is not None: auth_params.update({"DEVICE_KEY": self.device_key}) return auth_params @staticmethod def get_secret_hash(username, client_id, client_secret): message = bytearray(username + client_id, "utf-8") hmac_obj = hmac.new(bytearray(client_secret, "utf-8"), message, hashlib.sha256) return base64.standard_b64encode(hmac_obj.digest()).decode("utf-8") @staticmethod def get_cognito_formatted_timestamp(input_datetime): return f"{WEEKDAY_NAMES[input_datetime.weekday()]} {MONTH_NAMES[input_datetime.month - 1]} {input_datetime.day:d} {input_datetime.hour:02d}:{input_datetime.minute:02d}:{input_datetime.second:02d} UTC {input_datetime.year:d}" def process_challenge(self, challenge_parameters, request_parameters): internal_username = challenge_parameters.get( "USERNAME", request_parameters["USERNAME"] ) user_id_for_srp = challenge_parameters["USER_ID_FOR_SRP"] salt_hex = challenge_parameters["SALT"] srp_b_hex = challenge_parameters["SRP_B"] secret_block_b64 = challenge_parameters["SECRET_BLOCK"] timestamp = self.get_cognito_formatted_timestamp(datetime.datetime.utcnow()) hkdf = self.get_password_authentication_key( user_id_for_srp, self.password, hex_to_long(srp_b_hex), salt_hex ) secret_block_bytes = base64.standard_b64decode(secret_block_b64) msg = ( bytearray(self.pool_id.split("_")[1], "utf-8") + bytearray(user_id_for_srp, "utf-8") + bytearray(secret_block_bytes) + bytearray(timestamp, "utf-8") ) hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256) signature_string = base64.standard_b64encode(hmac_obj.digest()) response = { "TIMESTAMP": timestamp, "USERNAME": internal_username, "PASSWORD_CLAIM_SECRET_BLOCK": secret_block_b64, "PASSWORD_CLAIM_SIGNATURE": signature_string.decode("utf-8"), } if self.client_secret is not None: response.update( { "SECRET_HASH": self.get_secret_hash( internal_username, self.client_id, self.client_secret ) } ) if self.device_key is not None: response.update({"DEVICE_KEY": self.device_key}) return response def process_device_challenge(self, challenge_parameters): username = challenge_parameters["USERNAME"] salt_hex = challenge_parameters["SALT"] srp_b_hex = challenge_parameters["SRP_B"] secret_block_b64 = challenge_parameters["SECRET_BLOCK"] # re strips leading zero from a day number (required by AWS Cognito) timestamp = re.sub( r" 0(\d) ", r" \1 ", datetime.datetime.utcnow().strftime("%a %b %d %H:%M:%S UTC %Y"), ) hkdf = self.get_device_authentication_key( self.device_group_key, self.device_key, self.device_password, hex_to_long(srp_b_hex), salt_hex, ) secret_block_bytes = base64.standard_b64decode(secret_block_b64) msg = ( bytearray(self.device_group_key, "utf-8") + bytearray(self.device_key, "utf-8") + bytearray(secret_block_bytes) + bytearray(timestamp, "utf-8") ) hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256) signature_string = base64.standard_b64encode(hmac_obj.digest()) response = { "TIMESTAMP": timestamp, "USERNAME": username, "PASSWORD_CLAIM_SECRET_BLOCK": secret_block_b64, "PASSWORD_CLAIM_SIGNATURE": signature_string.decode("utf-8"), "DEVICE_KEY": self.device_key, } if self.client_secret is not None: response.update( { "SECRET_HASH": self.get_secret_hash( username, self.client_id, self.client_secret ) } ) return response def authenticate_user(self, client=None, client_metadata=None): boto_client = self.client or client auth_params = self.get_auth_params() response = boto_client.initiate_auth( AuthFlow="USER_SRP_AUTH", AuthParameters=auth_params, ClientId=self.client_id, ) if response["ChallengeName"] == self.PASSWORD_VERIFIER_CHALLENGE: challenge_response = self.process_challenge( response["ChallengeParameters"], auth_params ) tokens = boto_client.respond_to_auth_challenge( ClientId=self.client_id, ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE, ChallengeResponses=challenge_response, **({"ClientMetadata": client_metadata} if client_metadata else {}), ) if tokens.get("ChallengeName") == self.DEVICE_SRP_CHALLENGE: challenge_response = { "USERNAME": self.username, "DEVICE_KEY": self.device_key, "SRP_A": long_to_hex(self.large_a_value), } response = boto_client.respond_to_auth_challenge( ClientId=self.client_id, ChallengeName="DEVICE_SRP_AUTH", ChallengeResponses=challenge_response, ) challenge_response = self.process_device_challenge( response["ChallengeParameters"] ) tokens = boto_client.respond_to_auth_challenge( ClientId=self.client_id, ChallengeName="DEVICE_PASSWORD_VERIFIER", ChallengeResponses=challenge_response, ) return tokens if tokens.get("ChallengeName") == self.NEW_PASSWORD_REQUIRED_CHALLENGE: raise ForceChangePasswordException( "Change password before authenticating" ) if tokens.get("ChallengeName") == self.SMS_MFA_CHALLENGE: raise SMSMFAChallengeException("Do SMS MFA", tokens) if tokens.get("ChallengeName") == self.SOFTWARE_TOKEN_MFA_CHALLENGE: raise SoftwareTokenMFAChallengeException( "Do Software Token MFA", tokens ) return tokens raise NotImplementedError( f"The {response['ChallengeName']} challenge is not supported" ) def set_new_password_challenge(self, new_password, client=None): boto_client = self.client or client auth_params = self.get_auth_params() response = boto_client.initiate_auth( AuthFlow="USER_SRP_AUTH", AuthParameters=auth_params, ClientId=self.client_id, ) if response["ChallengeName"] == self.PASSWORD_VERIFIER_CHALLENGE: challenge_response = self.process_challenge( response["ChallengeParameters"], auth_params ) tokens = boto_client.respond_to_auth_challenge( ClientId=self.client_id, ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE, ChallengeResponses=challenge_response, ) if tokens["ChallengeName"] == self.NEW_PASSWORD_REQUIRED_CHALLENGE: challenge_parameters = response["ChallengeParameters"] challenge_response.update( { "USERNAME": challenge_parameters["USERNAME"], "NEW_PASSWORD": new_password, } ) new_password_response = boto_client.respond_to_auth_challenge( ClientId=self.client_id, ChallengeName=self.NEW_PASSWORD_REQUIRED_CHALLENGE, Session=tokens["Session"], ChallengeResponses=challenge_response, ) return new_password_response return tokens raise NotImplementedError( f"The {response['ChallengeName']} challenge is not supported" ) def confirm_device(self, tokens, device_name=None): self.access_token = tokens["AuthenticationResult"]["AccessToken"] self.device_key = tokens["AuthenticationResult"]["NewDeviceMetadata"][ "DeviceKey" ] self.device_group_key = tokens["AuthenticationResult"]["NewDeviceMetadata"][ "DeviceGroupKey" ] self.device_name = device_name self.cognito_idp_url = ( f"https://cognito-idp.{self.pool_id.split('_')[0]}.amazonaws.com/" ) device_password, device_secret_verifier_config = generate_hash_device( self.device_group_key, self.device_key ) if device_name is None: device_name = platform.node() headers = { "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": "AWSCognitoIdentityProviderService.ConfirmDevice", } data = { "AccessToken": self.access_token, "DeviceKey": self.device_key, "DeviceName": device_name, "DeviceSecretVerifierConfig": device_secret_verifier_config, } response = requests.post( self.cognito_idp_url, headers=headers, data=json.dumps(data), timeout=30 ) return response, device_password def update_device_status(self, is_remembered, access_token, device_key): self.cognito_idp_url = ( f"https://cognito-idp.{self.pool_id.split('_')[0]}.amazonaws.com/" ) self.access_token = access_token self.device_key = device_key headers = { "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": "AWSCognitoIdentityProviderService.UpdateDeviceStatus", } if is_remembered is True: status = "remembered" elif is_remembered is False: status = "not_remembered" data = { "AccessToken": self.access_token, "DeviceKey": self.device_key, "DeviceRememberedStatus": status, } response = requests.post( self.cognito_idp_url, headers=headers, data=json.dumps(data), timeout=30 ) return f"{response} : {response.json}" def forget_device(self, access_token, device_key): self.cognito_idp_url = ( f"https://cognito-idp.{self.pool_id.split('_')[0]}.amazonaws.com/" ) self.access_token = access_token self.device_key = device_key headers = { "Authorization": f"Bearer {self.access_token}", "Content-Type": "application/x-amz-json-1.1", "X-Amz-Target": "AWSCognitoIdentityProviderService.ForgetDevice", } data = {"AccessToken": self.access_token, "DeviceKey": self.device_key} response = requests.post( self.cognito_idp_url, headers=headers, data=json.dumps(data), timeout=30 ) return f"{response} : {response.json}" NabuCasa-pycognito-4f6c90a/pycognito/exceptions.py000066400000000000000000000015241462135417600223770ustar00rootroot00000000000000class WarrantException(Exception): """Base class for all pyCognito exceptions""" class ForceChangePasswordException(WarrantException): """Raised when the user is forced to change their password""" class TokenVerificationException(WarrantException): """Raised when token verification fails.""" class MFAChallengeException(WarrantException): """Raised when MFA is required.""" def __init__(self, message, tokens, *args, **kwargs): super().__init__(message, tokens, *args, **kwargs) self.message = message self._tokens = tokens def get_tokens(self): return self._tokens class SoftwareTokenMFAChallengeException(MFAChallengeException): """Raised when Software Token MFA is required.""" class SMSMFAChallengeException(MFAChallengeException): """Raised when SMS MFA is required.""" NabuCasa-pycognito-4f6c90a/pycognito/utils.py000066400000000000000000000061731462135417600213630ustar00rootroot00000000000000from enum import Enum import requests.auth import requests from . import Cognito class TokenType(str, Enum): ID_TOKEN = "id_token" ACCESS_TOKEN = "access_token" class RequestsSrpAuth(requests.auth.AuthBase): """ A Requests Auth Plugin to automatically populate Authorization header with a Cognito token. Example: ``` import requests from pycognito.utils import RequestsSrpAuth auth = RequestsSrpAuth( username='myusername', password='secret', user_pool_id='eu-west-1_1234567', client_id='4dn6jbcbhqcofxyczo3ms9z4cc', user_pool_region='eu-west-1', ) response = requests.get('http://test.com', auth=auth) ``` """ def __init__( self, username: str = None, password: str = None, user_pool_id: str = None, user_pool_region: str = None, client_id: str = None, cognito: Cognito = None, http_header: str = "Authorization", http_header_prefix: str = "Bearer ", auth_token_type: TokenType = TokenType.ACCESS_TOKEN, boto3_client_kwargs=None, ): """ :param username: Cognito User. Required if `cognito` not set :param password: Password of Cognito User. Required if `cognito` not set :param user_pool_id: Cognito User Pool. Required if `cognito` not set :param user_pool_region: Region of the Cognito User Pool. Required if `cognito` not set :param client_id: Cognito Client ID / Application. Required if :py:attr:`cognito` not set :param cognito: Provide a preconfigured `pycognito.Cognito` instead of `username`, `password` etc :param http_header: The HTTP Header to populate. Defaults to "Authorization" (Basic Authentication) :param http_header_prefix: Prefix a value before the token. Defaults to "Bearer ". (Note the space) :param auth_token_type: Whether to populate the header with ID or ACCESS_TOKEN. Defaults to "ACCESS_TOKEN" :param boto3_client_kwargs: Keyword args to pass to Boto3 for client creation """ if cognito: self.cognito_client = cognito else: self.cognito_client = Cognito( user_pool_id=user_pool_id, client_id=client_id, user_pool_region=user_pool_region, username=username, boto3_client_kwargs=boto3_client_kwargs, ) self.username = username self.__password = password self.http_header = http_header self.http_header_prefix = http_header_prefix self.token_type = auth_token_type def __call__(self, request: requests.Request): # If this is the first time in, we'll need to auth if not self.cognito_client.access_token: self.cognito_client.authenticate(password=self.__password) # Checks if token is expired and fetches a new token if available self.cognito_client.check_token(renew=True) token = getattr(self.cognito_client, self.token_type.value) request.headers[self.http_header] = self.http_header_prefix + token return request NabuCasa-pycognito-4f6c90a/pylintrc000066400000000000000000000023531462135417600174210ustar00rootroot00000000000000[MASTER] reports=no ignore=tests_* # Reasons disabled: # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # abstract-class-little-used - prevents from setting right foundation # abstract-class-not-used - is flaky, should not show up but does # unused-argument - generic callbacks and setup methods create a lot of warnings # global-statement - used for the on-demand requirement installation # redefined-variable-type - this is Python, we're duck typing! # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing generated-members=botocore.errorfactory disable= abstract-method, cyclic-import, duplicate-code, global-statement, locally-disabled, not-context-manager, too-few-public-methods, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-lines, too-many-locals, too-many-public-methods, too-many-return-statements, too-many-statements, unused-argument, missing-docstring, line-too-long, too-few-public-methods, too-many-locals, too-many-branches [EXCEPTIONS] overgeneral-exceptions=builtins.Exception NabuCasa-pycognito-4f6c90a/requirements_test.txt000066400000000000000000000002071462135417600221510ustar00rootroot00000000000000coverage==7.5.1 black==24.4.2 flake8==7.0.0 pylint==3.1.1 pytest==8.2.0 moto[cognitoidp]>=5.0.0 requests-mock==1.12.1 freezegun==1.5.1 NabuCasa-pycognito-4f6c90a/setup.cfg000066400000000000000000000005701462135417600174520ustar00rootroot00000000000000[isort] multi_line_output = 3 include_trailing_comma=True force_grid_wrap=0 line_length=88 indent = " " force_sort_within_sections = true sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER default_section = THIRDPARTY forced_separate = tests combine_as_imports = true use_parentheses = true [flake8] max-line-length = 88 ignore = E231, E501, W503, E203 NabuCasa-pycognito-4f6c90a/setup.py000066400000000000000000000023711462135417600173440ustar00rootroot00000000000000from pathlib import Path from setuptools import setup, find_packages VERSION = "2024.5.1" setup( name="pycognito", version=VERSION, description="Python class to integrate Boto3's Cognito client so it is easy to login users. With SRP support.", long_description=Path("README.md").read_text(), long_description_content_type="text/markdown", classifiers=[ "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", "Environment :: Web Environment", ], download_url="https://github.com/pvizeli/pycognito/tarball/" + VERSION, keywords="aws,cognito,api,gateway,serverless", author="Pascal Vizeli", author_email="pvizeli@syshack.ch", packages=find_packages(), url="https://github.com/pvizeli/pycognito", license="Apache License 2.0", install_requires=[ "boto3>=1.10.49", "envs>=1.3", "pyjwt[crypto]>=2.8.0", "requests>=2.22.0", ], include_package_data=True, python_requires=">=3.8", zip_safe=True, ) NabuCasa-pycognito-4f6c90a/tests.py000066400000000000000000000427701462135417600173550ustar00rootroot00000000000000import datetime import unittest import os.path from unittest.mock import patch import uuid import freezegun import moto import moto.cognitoidp import boto3 from botocore.exceptions import ParamValidationError from botocore.stub import Stubber from envs import env import requests import requests_mock from pycognito import Cognito, UserObj, GroupObj, TokenVerificationException from pycognito.aws_srp import AWSSRP from pycognito.utils import RequestsSrpAuth def _mock_authenticate_user(_, client=None, client_metadata=None): return { "AuthenticationResult": { "TokenType": "admin", "IdToken": "dummy_token", "AccessToken": "dummy_token", "RefreshToken": "dummy_token", } } def _mock_get_params(_): return {"USERNAME": "bob", "SRP_A": "srp"} def _mock_verify_tokens(self, token, id_name, token_use): if "wrong" in token: raise TokenVerificationException setattr(self, id_name, token) class UserObjTestCase(unittest.TestCase): def setUp(self): if env("USE_CLIENT_SECRET", "False") == "True": self.app_id = env("COGNITO_APP_WITH_SECRET_ID") else: self.app_id = env("COGNITO_APP_ID") self.cognito_user_pool_id = env("COGNITO_USER_POOL_ID", "us-east-1_123456789") self.username = env("COGNITO_TEST_USERNAME") self.user = Cognito( user_pool_id=self.cognito_user_pool_id, client_id=self.app_id, username=self.username, ) self.user_metadata = { "user_status": "CONFIRMED", "username": "bjones", } self.user_info = [ {"Name": "name", "Value": "Brian Jones"}, {"Name": "given_name", "Value": "Brian"}, {"Name": "birthdate", "Value": "12/7/1980"}, ] def test_init(self): user = UserObj("bjones", self.user_info, self.user, self.user_metadata) self.assertEqual(user.username, self.user_metadata.get("username")) self.assertEqual(user.name, self.user_info[0].get("Value")) self.assertEqual(user.user_status, self.user_metadata.get("user_status")) class GroupObjTestCase(unittest.TestCase): def setUp(self): if env("USE_CLIENT_SECRET", "False") == "True": self.app_id = env("COGNITO_APP_WITH_SECRET_ID") else: self.app_id = env("COGNITO_APP_ID") self.cognito_user_pool_id = env("COGNITO_USER_POOL_ID", "us-east-1_123456789") self.group_data = {"GroupName": "test_group", "Precedence": 1} self.cognito_obj = Cognito( user_pool_id=self.cognito_user_pool_id, client_id=self.app_id ) def test_init(self): group = GroupObj(group_data=self.group_data, cognito_obj=self.cognito_obj) self.assertEqual(group.group_name, "test_group") self.assertEqual(group.precedence, 1) class CognitoAuthTestCase(unittest.TestCase): def setUp(self): if env("USE_CLIENT_SECRET") == "True": self.app_id = env("COGNITO_APP_WITH_SECRET_ID", "app") self.client_secret = env("COGNITO_CLIENT_SECRET") else: self.app_id = env("COGNITO_APP_ID", "app") self.client_secret = None self.cognito_user_pool_id = env("COGNITO_USER_POOL_ID", "us-east-1_123456789") self.username = env("COGNITO_TEST_USERNAME", "bob") self.password = env("COGNITO_TEST_PASSWORD", "bobpassword") self.user = Cognito( self.cognito_user_pool_id, self.app_id, username=self.username, client_secret=self.client_secret, ) @patch("pycognito.aws_srp.AWSSRP.authenticate_user", _mock_authenticate_user) @patch("pycognito.Cognito.verify_token", _mock_verify_tokens) def test_authenticate(self): self.user.authenticate(self.password) self.assertNotEqual(self.user.access_token, None) self.assertNotEqual(self.user.id_token, None) self.assertNotEqual(self.user.refresh_token, None) @patch("pycognito.aws_srp.AWSSRP.authenticate_user", _mock_authenticate_user) @patch("pycognito.Cognito.verify_token", _mock_verify_tokens) def test_verify_token(self): self.user.authenticate(self.password) bad_access_token = "{}wrong".format(self.user.access_token) with self.assertRaises(TokenVerificationException): self.user.verify_token(bad_access_token, "access_token", "access") @patch("pycognito.Cognito", autospec=True) def test_register(self, cognito_user): user = cognito_user( self.cognito_user_pool_id, self.app_id, username=self.username ) base_attr = dict( given_name="Brian", family_name="Jones", name="Brian Jones", email="bjones39@capless.io", phone_number="+19194894555", gender="Male", preferred_username="billyocean", ) user.set_base_attributes(**base_attr) user.register("sampleuser", "sample4#Password") @patch("pycognito.aws_srp.AWSSRP.authenticate_user", _mock_authenticate_user) @patch("pycognito.Cognito.verify_token", _mock_verify_tokens) @patch("pycognito.Cognito._add_secret_hash", return_value=None) def test_renew_tokens(self, _): stub = Stubber(self.user.client) # By the stubber nature, we need to add the sequence # of calls for the AWS SRP auth to test the whole process stub.add_response( method="initiate_auth", service_response={ "AuthenticationResult": { "TokenType": "admin", "IdToken": "dummy_token", "AccessToken": "dummy_token", "RefreshToken": "dummy_token", }, "ResponseMetadata": {"HTTPStatusCode": 200}, }, expected_params={ "ClientId": self.app_id, "AuthFlow": "REFRESH_TOKEN_AUTH", "AuthParameters": {"REFRESH_TOKEN": "dummy_token"}, }, ) with stub: self.user.authenticate(self.password) self.user.renew_access_token() stub.assert_no_pending_responses() @patch("pycognito.Cognito", autospec=True) def test_update_profile(self, cognito_user): user = cognito_user( self.cognito_user_pool_id, self.app_id, username=self.username ) user.authenticate(self.password) user.update_profile({"given_name": "Jenkins"}) def test_admin_get_user(self): stub = Stubber(self.user.client) stub.add_response( method="admin_get_user", service_response={ "Enabled": True, "UserStatus": "CONFIRMED", "Username": self.username, "UserAttributes": [], }, expected_params={ "UserPoolId": self.cognito_user_pool_id, "Username": self.username, }, ) with stub: u = self.user.admin_get_user() self.assertEqual(u.username, self.username) stub.assert_no_pending_responses() def test_check_token(self): # This is a sample JWT with an expiration time set to January, 1st, 3000 self.user.access_token = ( "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG" "9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjMyNTAzNjgwMDAwfQ.C-1gPxrhUsiWeCvMvaZuuQYarkDNAc" "pEGJPIqu_SrKQ" ) try: valid = self.user.check_token() except OverflowError: self.skipTest("This test requires 64-bit time_t") else: self.assertFalse(valid) @patch("pycognito.Cognito", autospec=True) def test_validate_verification(self, cognito_user): u = cognito_user(self.cognito_user_pool_id, self.app_id, username=self.username) u.validate_verification("4321") @patch("pycognito.Cognito", autospec=True) def test_confirm_forgot_password(self, cognito_user): u = cognito_user(self.cognito_user_pool_id, self.app_id, username=self.username) u.confirm_forgot_password("4553", "samplepassword") with self.assertRaises(TypeError): u.confirm_forgot_password(self.password) @patch("pycognito.aws_srp.AWSSRP.authenticate_user", _mock_authenticate_user) @patch("pycognito.Cognito.verify_token", _mock_verify_tokens) @patch("pycognito.Cognito.check_token", return_value=True) def test_change_password(self, _): # u = cognito_user(self.cognito_user_pool_id, self.app_id, # username=self.username) self.user.authenticate(self.password) stub = Stubber(self.user.client) stub.add_response( method="change_password", service_response={"ResponseMetadata": {"HTTPStatusCode": 200}}, expected_params={ "PreviousPassword": self.password, "ProposedPassword": "crazypassword$45DOG", "AccessToken": self.user.access_token, }, ) with stub: self.user.change_password(self.password, "crazypassword$45DOG") stub.assert_no_pending_responses() with self.assertRaises(ParamValidationError): self.user.change_password(self.password, None) def test_set_attributes(self): user = Cognito(self.cognito_user_pool_id, self.app_id) user._set_attributes( {"ResponseMetadata": {"HTTPStatusCode": 200}}, {"somerandom": "attribute"} ) self.assertEqual(user.somerandom, "attribute") @patch("pycognito.Cognito.verify_token", _mock_verify_tokens) def test_admin_authenticate(self): stub = Stubber(self.user.client) # By the stubber nature, we need to add the sequence # of calls for the AWS SRP auth to test the whole process stub.add_response( method="admin_initiate_auth", service_response={ "AuthenticationResult": { "TokenType": "admin", "IdToken": "dummy_token", "AccessToken": "dummy_token", "RefreshToken": "dummy_token", } }, expected_params={ "UserPoolId": self.cognito_user_pool_id, "ClientId": self.app_id, "AuthFlow": "ADMIN_NO_SRP_AUTH", "AuthParameters": { "USERNAME": self.username, "PASSWORD": self.password, }, }, ) with stub: self.user.admin_authenticate(self.password) self.assertNotEqual(self.user.access_token, None) self.assertNotEqual(self.user.id_token, None) self.assertNotEqual(self.user.refresh_token, None) stub.assert_no_pending_responses() class AWSSRPTestCase(unittest.TestCase): def setUp(self): if env("USE_CLIENT_SECRET") == "True": self.client_secret = env("COGNITO_CLIENT_SECRET") self.app_id = env("COGNITO_APP_WITH_SECRET_ID", "app") else: self.app_id = env("COGNITO_APP_ID", "app") self.client_secret = None self.cognito_user_pool_id = env("COGNITO_USER_POOL_ID", "us-east-1_123456789") self.username = env("COGNITO_TEST_USERNAME") self.password = env("COGNITO_TEST_PASSWORD") self.aws = AWSSRP( username=self.username, password=self.password, pool_region="us-east-1", pool_id=self.cognito_user_pool_id, client_id=self.app_id, client_secret=self.client_secret, ) def tearDown(self): del self.aws @patch("pycognito.aws_srp.AWSSRP.get_auth_params", _mock_get_params) @patch("pycognito.aws_srp.AWSSRP.process_challenge", return_value={}) def test_authenticate_user(self, _): stub = Stubber(self.aws.client) # By the stubber nature, we need to add the sequence # of calls for the AWS SRP auth to test the whole process stub.add_response( method="initiate_auth", service_response={ "ChallengeName": "PASSWORD_VERIFIER", "ChallengeParameters": {}, }, expected_params={ "AuthFlow": "USER_SRP_AUTH", "AuthParameters": _mock_get_params(None), "ClientId": self.app_id, }, ) stub.add_response( method="respond_to_auth_challenge", service_response={ "AuthenticationResult": { "IdToken": "dummy_token", "AccessToken": "dummy_token", "RefreshToken": "dummy_token", } }, expected_params={ "ClientId": self.app_id, "ChallengeName": "PASSWORD_VERIFIER", "ChallengeResponses": {}, }, ) with stub: tokens = self.aws.authenticate_user() self.assertTrue("IdToken" in tokens["AuthenticationResult"]) self.assertTrue("AccessToken" in tokens["AuthenticationResult"]) self.assertTrue("RefreshToken" in tokens["AuthenticationResult"]) stub.assert_no_pending_responses() def test_cognito_formatted_timestamp(self): self.assertEqual( self.aws.get_cognito_formatted_timestamp( datetime.datetime(2022, 1, 1, 0, 0, 0) ), "Sat Jan 1 00:00:00 UTC 2022", ) self.assertEqual( self.aws.get_cognito_formatted_timestamp( datetime.datetime(2022, 1, 2, 12, 0, 0) ), "Sun Jan 2 12:00:00 UTC 2022", ) self.assertEqual( self.aws.get_cognito_formatted_timestamp( datetime.datetime(2022, 1, 3, 9, 0, 0) ), "Mon Jan 3 09:00:00 UTC 2022", ) self.assertEqual( self.aws.get_cognito_formatted_timestamp( datetime.datetime(2022, 12, 31, 23, 59, 59) ), "Sat Dec 31 23:59:59 UTC 2022", ) @moto.mock_aws class UtilsTestCase(unittest.TestCase): username = "bob@test.com" password = "Test1234!" def setUp(self) -> None: cognitoidp_client = boto3.client("cognito-idp", region_name="us-east-1") user_pool = cognitoidp_client.create_user_pool( PoolName="pycognito-test-pool", AliasAttributes=[ "email", ], UsernameAttributes=[ "email", ], ) self.user_pool_id = user_pool["UserPool"]["Id"] user_pool_client = cognitoidp_client.create_user_pool_client( UserPoolId=self.user_pool_id, ClientName="test-client", RefreshTokenValidity=1, AccessTokenValidity=1, IdTokenValidity=1, TokenValidityUnits={ "AccessToken": "hour", "IdToken": "hour", "RefreshToken": "days", }, ) self.client_id = user_pool_client["UserPoolClient"]["ClientId"] cognitoidp_client.admin_create_user( UserPoolId=self.user_pool_id, Username=self.username, TemporaryPassword=self.password, MessageAction="SUPPRESS", ) cognitoidp_client.admin_set_user_password( UserPoolId=self.user_pool_id, Username=self.username, Password=self.password, Permanent=True, ) @requests_mock.Mocker() def test_srp_requests_http_auth(self, m): # Get Moto's static public jwks jwks_public_key_filename = os.path.join( os.path.dirname(moto.cognitoidp.__file__), "resources/jwks-public.json" ) jwks_public_key_f = open(jwks_public_key_filename, "rb") # Create some test data test_data = str(uuid.uuid4()) # Mock a test endpoint. We pretend this endpoint would require an Authorization header m.get("http://test.com", text=test_data) # Pycognito will automatically verify the token it receives. Mock the proper endpoint and return the static # key from above m.get( f"https://cognito-idp.us-east-1.amazonaws.com/{self.user_pool_id}/.well-known/jwks.json", body=jwks_public_key_f, ) now = datetime.datetime.utcnow() # Standup the actual Requests plugin srp_auth = RequestsSrpAuth( username=self.username, password=self.password, user_pool_id=self.user_pool_id, user_pool_region="us-east-1", client_id=self.client_id, ) # Make the actual request req = requests.get("http://test.com", auth=srp_auth) req.raise_for_status() # Ensure the data returns matches the mocked endpoint self.assertEqual(test_data, req.text) # Get the access token used access_token_orig = srp_auth.cognito_client.access_token # Make a second request with a time 2 hours in the future with freezegun.freeze_time(now + datetime.timedelta(hours=2)): req = requests.get("http://test.com", auth=srp_auth) req.raise_for_status() access_token_new = srp_auth.cognito_client.access_token # Check that the access token was refreshed to a new one self.assertNotEqual(access_token_orig, access_token_new) if __name__ == "__main__": unittest.main() NabuCasa-pycognito-4f6c90a/tox.ini000066400000000000000000000005711462135417600171450ustar00rootroot00000000000000[tox] envlist = lint, tests, black [testenv] basepython = python3 deps = -r{toxinidir}/requirements_test.txt [testenv:lint] ignore_errors = True commands = flake8 pycognito tests.py pylint --rcfile pylintrc pycognito [testenv:black] commands = black --target-version py36 --check pycognito tests.py setup.py [testenv:tests] commands = pytest tests.py