Repository: cevoaustralia/aws-google-auth Branch: master Commit: dd42263bbeeb Files: 37 Total size: 226.8 KB Directory structure: gitextract_w3wj90cw/ ├── .github/ │ └── workflows/ │ ├── pythonpackage.yml │ ├── pythonrelease.yml │ └── rstlint.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.python2 ├── LICENSE.txt ├── README.rst ├── aws_google_auth/ │ ├── __init__.py │ ├── _version.py │ ├── amazon.py │ ├── configuration.py │ ├── google.py │ ├── tests/ │ │ ├── __init__.py │ │ ├── google_error.html │ │ ├── saml-response-expired-before-valid.xml │ │ ├── saml-response-no-expire.xml │ │ ├── saml-response-too-late.xml │ │ ├── saml-response-too-soon.xml │ │ ├── test_amazon.py │ │ ├── test_args_parser.py │ │ ├── test_backwards_compatibility.py │ │ ├── test_config_parser.py │ │ ├── test_configuration.py │ │ ├── test_configuration_persistence.py │ │ ├── test_google.py │ │ ├── test_init.py │ │ ├── test_python_version.py │ │ ├── test_util.py │ │ ├── too-many-commas.xml │ │ └── valid-response.xml │ ├── u2f.py │ └── util.py ├── requirements.txt └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/pythonpackage.yml ================================================ # This workflow will install Python dependencies, run tests and lint with a variety of Python versions # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python package on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install . pip install -r requirements.txt - name: Lint with flake8 run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 --ignore E501,E722 # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} run: | pip install coverage pip install coveralls pip install pytest coverage run --source=aws_google_auth/ --omit=aws_google_auth/tests/* setup.py test coverage report coveralls ================================================ FILE: .github/workflows/pythonrelease.yml ================================================ # This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python setup.py sdist bdist_wheel twine upload dist/* ================================================ FILE: .github/workflows/rstlint.yml ================================================ name: Lint RST on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install Pygments restructuredtext_lint - name: Lint with rst run: | rst-lint README.rst ================================================ FILE: .gitignore ================================================ *.swp *.egg-info .eggs/ *.pyc dist build/ .idea/ Pipfile Pipfile.lock venv/* ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at opensource@cevo.com.au. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Contributions are welcome! The most valuable contributions, in order of preference, are: 1. Pull requests (whether adding a feature, improving an existing feature, or fixing a bug) 1. Opening an issue (bug reports or feature requests) 1. Fork, star, watch, or share this project on your social networks. ## Pull Requests Pull requests are definitely welcome. In order to be most useful, please try and make sure that: * the pull request has a clear description of what it's for (new feature, enhancement, or bug fix) * the code is clean and understandable * the pull request would merge cleanly ## Issues Issues are also very welcome! Please try and make sure that: * bug reports include stack traces, copied and pasted from your terminal * feature requests include a clear description of _why_ you want that feature, not just what you want ## Thanks! Thanks for checking out this project. While you're here, have a look at some of the other tools, bits and pieces we've created under https://github.com/cevoaustralia ================================================ FILE: Dockerfile ================================================ FROM alpine:3.5 RUN apk add --update-cache py3-pip ca-certificates py3-certifi py3-lxml\ python3-dev cython cython-dev libusb-dev build-base \ eudev-dev linux-headers libffi-dev openssl-dev \ jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev \ tiff-dev tk-dev tcl-dev COPY setup.py README.rst requirements.txt /build/ RUN pip3 install -r /build/requirements.txt COPY aws_google_auth /build/aws_google_auth RUN pip3 install -e /build/[u2f] ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt ENTRYPOINT ["aws-google-auth"] ================================================ FILE: Dockerfile.python2 ================================================ FROM alpine:3.5 RUN apk add --update-cache py2-pip ca-certificates py2-certifi py2-lxml \ python-dev cython cython-dev libusb-dev build-base \ eudev-dev linux-headers libffi-dev openssl-dev \ jpeg-dev zlib-dev freetype-dev lcms2-dev openjpeg-dev \ tiff-dev tk-dev tcl-dev ADD . /build/ RUN pip install -e /build/[u2f] ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt ENTRYPOINT ["aws-google-auth"] ================================================ FILE: LICENSE.txt ================================================ MIT License Copyright (c) 2016 Cevo Australia, Pty Ltd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.rst ================================================ aws-google-auth =============== |github-badge| |docker-badge| |pypi-badge| |coveralls-badge| .. |github-badge| image:: https://github.com/cevoaustralia/aws-google-auth/workflows/Python%20package/badge.svg :target: https://github.com/cevoaustralia/aws-google-auth/actions :alt: GitHub build badge .. |docker-badge| image:: https://img.shields.io/docker/build/cevoaustralia/aws-google-auth.svg :target: https://hub.docker.com/r/cevoaustralia/aws-google-auth/ :alt: Docker build status badge .. |pypi-badge| image:: https://img.shields.io/pypi/v/aws-google-auth.svg :target: https://pypi.python.org/pypi/aws-google-auth/ :alt: PyPI version badge .. |coveralls-badge| image:: https://coveralls.io/repos/github/cevoaustralia/aws-google-auth/badge.svg?branch=master :target: https://coveralls.io/github/cevoaustralia/aws-google-auth?branch=master This command-line tool allows you to acquire AWS temporary (STS) credentials using Google Apps as a federated (Single Sign-On, or SSO) provider. Setup ----- You'll first have to set up Google Apps as a SAML identity provider (IdP) for AWS. There are tasks to be performed on both the Google Apps and the Amazon sides; these references should help you with those configurations: - `How to Set Up Federated Single Sign-On to AWS Using Google Apps `__ - `Using Google Apps SAML SSO to do one-click login to AWS `__ If you need a fairly simple way to assign users to roles in AWS accounts, we have another tool called `Google AWS Federator `__ that might help you. Important Data ~~~~~~~~~~~~~~ You will need to know Google's assigned Identity Provider ID, and the ID that they assign to the SAML service provider. Once you've set up the SAML SSO relationship between Google and AWS, you can find the SP ID by drilling into the Google Apps console, under ``Apps > SAML Apps > Settings for AWS SSO`` -- the URL will include a component that looks like ``...#AppDetails:service=123456789012...`` -- that number is ``GOOGLE_SP_ID`` You can find the ``GOOGLE_IDP_ID``, again from the admin console, via ``Security > Set up single sign-on (SSO)`` -- the ``SSO URL`` includes a string like ``https://accounts.google.com/o/saml2/idp?idpid=aBcD01AbC`` where the last bit (after the ``=``) is the IDP ID. Installation ------------ You can install quite easily via ``pip``, if you want to have it on your local system: .. code:: shell # For basic installation localhost$ sudo pip install aws-google-auth # For installation with U2F support localhost$ sudo pip install aws-google-auth[u2f] *Note* If using ZSH you will need to quote the install, as below: .. code:: shell localhost$ sudo pip install "aws-google-auth[u2f]" If you don't want to have the tool installed on your local system, or if you prefer to isolate changes, there is a Dockerfile provided, which you can build with: .. code:: shell # Perform local build localhost$ cd ..../aws-google-auth && docker build -t aws-google-auth . # Use the Docker Hub version localhost$ docker pull cevoaustralia/aws-google-auth Development ----------- If you want to develop the AWS-Google-Auth tool itself, we thank you! In order to help you get rolling, you'll want to install locally with pip. Of course, you can use your own regular workflow, with tools like `virtualenv `__. .. code:: shell # Install (without U2F support) pip install -e . # Install (with U2F support) pip install -e .[u2f] We welcome you to review our `code of conduct `__ and `contributing `__ documents. Usage ----- .. code:: shell $ aws-google-auth -h usage: aws-google-auth [-h] [-u USERNAME] [-I IDP_ID] [-S SP_ID] [-R REGION] [-d DURATION] [-p PROFILE] [-D] [-q] [--bg-response BG_RESPONSE] [--saml-assertion SAML_ASSERTION] [--no-cache] [--print-creds] [--resolve-aliases] [--save-failure-html] [--save-saml-flow] [-a | -r ROLE_ARN] [-k] [-l {debug,info,warn}] [-V] Acquire temporary AWS credentials via Google SSO optional arguments: -h, --help show this help message and exit -u USERNAME, --username USERNAME Google Apps username ($GOOGLE_USERNAME) -I IDP_ID, --idp-id IDP_ID Google SSO IDP identifier ($GOOGLE_IDP_ID) -S SP_ID, --sp-id SP_ID Google SSO SP identifier ($GOOGLE_SP_ID) -R REGION, --region REGION AWS region endpoint ($AWS_DEFAULT_REGION) -d DURATION, --duration DURATION Credential duration (defaults to value of $DURATION, then falls back to 43200) -p PROFILE, --profile PROFILE AWS profile (defaults to value of $AWS_PROFILE, then falls back to 'sts') -D, --disable-u2f Disable U2F functionality. -q, --quiet Quiet output --bg-response BG_RESPONSE Override default bgresponse challenge token ($GOOGLE_BG_RESPONSE). --saml-assertion SAML_ASSERTION Base64 encoded SAML assertion to use. --no-cache Do not cache the SAML Assertion. --print-creds Print Credentials. --resolve-aliases Resolve AWS account aliases. --save-failure-html Write HTML failure responses to file for troubleshooting. --save-saml-flow Write all GET and PUT requests and HTML responses to/from Google to files for troubleshooting. -a, --ask-role Set true to always pick the role -r ROLE_ARN, --role-arn ROLE_ARN The ARN of the role to assume ($AWS_ROLE_ARN) -k, --keyring Use keyring for storing the password. -l {debug,info,warn}, --log {debug,info,warn} Select log level (default: warn) -V, --version show program's version number and exit **Note** If you want a longer session than the AWS default 3600 seconds (1 hour) duration, you must also modify the IAM Role to permit this. See `the AWS documentation `__ for more information. Native Python ~~~~~~~~~~~~~ 1. Execute ``aws-google-auth`` 2. You will be prompted to supply each parameter *Note* You can skip prompts by either passing parameters to the command, or setting the specified Environment variables. Via Docker ~~~~~~~~~~~~~ 1. Set environment variables for anything listed in Usage with ``($VARIABLE)`` after command line option: ``GOOGLE_USERNAME``, ``GOOGLE_IDP_ID``, and ``GOOGLE_SP_ID`` (see above under "Important Data" for how to find the last two; the first one is usually your email address) ``AWS_PROFILE``: Optional profile name you want the credentials set for (default is 'sts') ``ROLE_ARN``: Optional ARN of the role to assume 2. For Docker: ``docker run -it -e GOOGLE_USERNAME -e GOOGLE_IDP_ID -e GOOGLE_SP_ID -e AWS_PROFILE -e ROLE_ARN -v ~/.aws:/root/.aws cevoaustralia/aws-google-auth`` You'll be prompted for your password. If you've set up an MFA token for your Google account, you'll also be prompted for the current token value. If you have a U2F security key added to your Google account, you won't be able to use this via Docker; the Docker container will not be able to access any devices connected to the host ports. You will likely see the following error during runtime: "RuntimeWarning: U2F Device Not Found". If you have more than one role available to you (and you haven't set up ROLE_ARN), you'll be prompted to choose the role from a list. Feeding password from stdin ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To enhance usability when using third party tools for managing passwords (aka password manager) you can feed data in ``aws-google-auth`` from ``stdin``. When receiving data from ``stdin`` ``aws-google-auth`` disables the interactive prompt and uses ``stdin`` data. Before `#82 `_, all interactive prompts could be fed from ``stdin`` already apart from the ``Google Password:`` prompt. Example usage: ``` $ password-manager show password | aws-google-auth Google Password: MFA token: Assuming arn:aws:iam::123456789012:role/admin Credentials Expiration: ... ``` **Note:** this feature is intended for password manager integration, not for passing passwords from command line. Please use interactive prompt if you need to pass the password manually, as this provide enhanced security avoid password leakage to shell history. Storage of profile credentials ------------------------------ Through the use of AWS profiles, using the ``-p`` or ``--profile`` flag, the ``aws-google-auth`` utility will store the supplied username, IDP and SP details in your ``./aws/config`` files. When re-authenticating using the same profile, the values will be remembered to speed up the re-authentication process. This enables an approach that enables you to enter your username, IPD and SP values once and then after only need to re-enter your password (and MFA if enabled). Creating an alias as below can be a quick and easy way to re-authenticate with a simple command shortcut. ``` alias aws-development='unset AWS_PROFILE; aws-google-auth -I $GOOGLE_IDP_ID -S $GOOGLE_SP_ID -u $USERNAME -p aws-dev ; export AWS_PROFILE=aws-dev' ``` Or, if you've alredy established a profile with valid cached values: ``` alias aws-development='unset AWS_PROFILE; aws-google-auth -p aws-dev ; export AWS_PROFILE=aws-dev' ``` Notes on Authentication ----------------------- Google supports a number of 2-factor authentication schemes. Each of these results in a slightly different "next" URL, if they're enabled, during ``do_login`` Google controls the preference ordering of these schemes in the case that you have multiple ones defined. The varying 2-factor schemes and their representative URL fragments handled by this tool are: +------------------+-------------------------------------+ | Method | URL Fragment | +==================+=====================================+ | No second factor | (none) | +------------------+-------------------------------------+ | TOTP (eg Google | ``.../signin/challenge/totp/...`` | | Authenticator | | | or Authy) | | +------------------+-------------------------------------+ | SMS (or voice | ``.../signin/challenge/ipp/...`` | | call) | | +------------------+-------------------------------------+ | SMS (or voice | ``.../signin/challenge/iap/...`` | | call) with | | | number | | | submission | | +------------------+-------------------------------------+ | Google Prompt | ``.../signin/challenge/az/...`` | | (phone app) | | +------------------+-------------------------------------+ | Security key | ``.../signin/challenge/sk/...`` | | (eg yubikey) | | +------------------+-------------------------------------+ | Dual prompt | ``.../signin/challenge/dp/...`` | | (Validate 2FA ) | | +------------------+-------------------------------------+ | Backup code | ``... (unknown yet) ...`` | | (printed codes) | | +------------------+-------------------------------------+ Acknowledgments ---------------- This work is inspired by `keyme `__ -- their digging into the guts of how Google SAML auth works is what's enabled it. The attribute management and credential injection into AWS configuration files was heavily borrowed from `aws-adfs ` ================================================ FILE: aws_google_auth/__init__.py ================================================ #!/usr/bin/env python from __future__ import print_function import argparse import base64 import os import sys import logging import keyring from six import print_ as print from tzlocal import get_localzone from aws_google_auth import _version from aws_google_auth import amazon from aws_google_auth import configuration from aws_google_auth import google from aws_google_auth import util def parse_args(args): parser = argparse.ArgumentParser( prog="aws-google-auth", description="Acquire temporary AWS credentials via Google SSO", ) parser.add_argument('-u', '--username', help='Google Apps username ($GOOGLE_USERNAME)') parser.add_argument('-I', '--idp-id', help='Google SSO IDP identifier ($GOOGLE_IDP_ID)') parser.add_argument('-S', '--sp-id', help='Google SSO SP identifier ($GOOGLE_SP_ID)') parser.add_argument('-R', '--region', help='AWS region endpoint ($AWS_DEFAULT_REGION)') duration_group = parser.add_mutually_exclusive_group() duration_group.add_argument('-d', '--duration', type=int, help='Credential duration in seconds (defaults to value of $DURATION, then falls back to 43200)') duration_group.add_argument('--auto-duration', action='store_true', help='Tries to use the longest allowed duration ($AUTO_DURATION)') parser.add_argument('-p', '--profile', help='AWS profile (defaults to value of $AWS_PROFILE, then falls back to \'sts\')') parser.add_argument('-A', '--account', help='Filter for specific AWS account.') parser.add_argument('-D', '--disable-u2f', action='store_true', help='Disable U2F functionality.') parser.add_argument('-q', '--quiet', action='store_true', help='Quiet output') parser.add_argument('--bg-response', help='Override default bgresponse challenge token.') parser.add_argument('--saml-assertion', dest="saml_assertion", help='Base64 encoded SAML assertion to use.') parser.add_argument('--no-cache', dest="saml_cache", action='store_false', help='Do not cache the SAML Assertion.') parser.add_argument('--print-creds', action='store_true', help='Print Credentials.') parser.add_argument('--resolve-aliases', action='store_true', help='Resolve AWS account aliases.') parser.add_argument('--save-failure-html', action='store_true', help='Write HTML failure responses to file for troubleshooting.') parser.add_argument('--save-saml-flow', action='store_true', help='Write all GET and PUT requests and HTML responses to/from Google to files for troubleshooting.') role_group = parser.add_mutually_exclusive_group() role_group.add_argument('-a', '--ask-role', action='store_true', help='Set true to always pick the role') role_group.add_argument('-r', '--role-arn', help='The ARN of the role to assume') parser.add_argument('-k', '--keyring', action='store_true', help='Use keyring for storing the password.') parser.add_argument('-l', '--log', dest='log_level', choices=['debug', 'info', 'warn'], default='warn', help='Select log level (default: %(default)s)') parser.add_argument('-V', '--version', action='version', version='%(prog)s {version}'.format(version=_version.__version__)) return parser.parse_args(args) def exit_if_unsupported_python(): if sys.version_info.major == 2 and sys.version_info.minor < 7: logging.critical("%s requires Python 2.7 or higher. Please consider " "upgrading. Support for Python 2.6 and lower was " "dropped because this tool's dependencies dropped " "support.", __name__) logging.critical("For debugging, it appears you're running: %s", sys.version_info) logging.critical("For more information, see: " "https://github.com/cevoaustralia/aws-google-auth/" "issues/41") sys.exit(1) def cli(cli_args): try: exit_if_unsupported_python() args = parse_args(args=cli_args) config = resolve_config(args) process_auth(args, config) except google.ExpectedGoogleException as ex: print(ex) sys.exit(1) except KeyboardInterrupt: pass except Exception as ex: logging.exception(ex) def resolve_config(args): # Shortening Convenience functions coalesce = util.Util.coalesce # Create a blank configuration object (has the defaults pre-filled) config = configuration.Configuration() # Have the configuration update itself via the ~/.aws/config on disk. # Profile (Option priority = ARGS, ENV_VAR, DEFAULT) config.profile = coalesce( args.profile, os.getenv('AWS_PROFILE'), config.profile) # Now that we've established the profile, we can read the configuration and # fill in all the other variables. config.read(config.profile) # Ask Role (Option priority = ARGS, ENV_VAR, DEFAULT) config.ask_role = bool(coalesce( args.ask_role, os.getenv('AWS_ASK_ROLE'), config.ask_role)) # Duration (Option priority = ARGS, ENV_VAR, DEFAULT) config.duration = int(coalesce( args.duration, os.getenv('DURATION'), config.duration)) # Automatic duration (Option priority = ARGS, ENV_VAR, DEFAULT) config.auto_duration = coalesce( args.auto_duration, os.getenv('AUTO_DURATION'), config.auto_duration ) # IDP ID (Option priority = ARGS, ENV_VAR, DEFAULT) config.idp_id = coalesce( args.idp_id, os.getenv('GOOGLE_IDP_ID'), config.idp_id) # Region (Option priority = ARGS, ENV_VAR, DEFAULT) config.region = coalesce( args.region, os.getenv('AWS_DEFAULT_REGION'), config.region) # ROLE ARN (Option priority = ARGS, ENV_VAR, DEFAULT) config.role_arn = coalesce( args.role_arn, os.getenv('AWS_ROLE_ARN'), config.role_arn) # SP ID (Option priority = ARGS, ENV_VAR, DEFAULT) config.sp_id = coalesce( args.sp_id, os.getenv('GOOGLE_SP_ID'), config.sp_id) # U2F Disabled (Option priority = ARGS, ENV_VAR, DEFAULT) config.u2f_disabled = coalesce( args.disable_u2f, os.getenv('U2F_DISABLED'), config.u2f_disabled) # Resolve AWS aliases enabled (Option priority = ARGS, ENV_VAR, DEFAULT) config.resolve_aliases = coalesce( args.resolve_aliases, os.getenv('RESOLVE_AWS_ALIASES'), config.resolve_aliases) # Username (Option priority = ARGS, ENV_VAR, DEFAULT) config.username = coalesce( args.username, os.getenv('GOOGLE_USERNAME'), config.username) # Account (Option priority = ARGS, ENV_VAR, DEFAULT) config.account = coalesce( args.account, os.getenv('AWS_ACCOUNT'), config.account) config.keyring = coalesce( args.keyring, config.keyring) config.print_creds = coalesce( args.print_creds, config.print_creds) # Quiet config.quiet = coalesce( args.quiet, config.quiet) config.bg_response = coalesce( args.bg_response, os.getenv('GOOGLE_BG_RESPONSE'), config.bg_response) return config def process_auth(args, config): # Set up logging logging.getLogger().setLevel(getattr(logging, args.log_level.upper(), None)) if config.region is None: config.region = util.Util.get_input("AWS Region: ") logging.debug('%s: region is: %s', __name__, config.region) # If there is a valid cache and the user opted to use it, use that instead # of prompting the user for input (it will also ignroe any set variables # such as username or sp_id and idp_id, as those are built into the SAML # response). The user does not need to be prompted for a password if the # SAML cache is used. if args.saml_assertion: saml_xml = base64.b64decode(args.saml_assertion) elif args.saml_cache and config.saml_cache: saml_xml = config.saml_cache logging.info('%s: SAML cache found', __name__) else: # No cache, continue without. logging.info('%s: SAML cache not found', __name__) if config.username is None: config.username = util.Util.get_input("Google username: ") logging.debug('%s: username is: %s', __name__, config.username) if config.idp_id is None: config.idp_id = util.Util.get_input("Google IDP ID: ") logging.debug('%s: idp is: %s', __name__, config.idp_id) if config.sp_id is None: config.sp_id = util.Util.get_input("Google SP ID: ") logging.debug('%s: sp is: %s', __name__, config.sp_id) # There is no way (intentional) to pass in the password via the command # line nor environment variables. This prevents password leakage. keyring_password = None if config.keyring: keyring_password = keyring.get_password("aws-google-auth", config.username) if keyring_password: config.password = keyring_password else: config.password = util.Util.get_password("Google Password: ") else: config.password = util.Util.get_password("Google Password: ") # Validate Options config.raise_if_invalid() google_client = google.Google(config, save_failure=args.save_failure_html, save_flow=args.save_saml_flow) google_client.do_login() saml_xml = google_client.parse_saml() logging.debug('%s: saml assertion is: %s', __name__, saml_xml) # If we logged in correctly and we are using keyring then store the password if config.keyring and keyring_password is None: keyring.set_password( "aws-google-auth", config.username, config.password) # We now have a new SAML value that can get cached (If the user asked # for it to be) if args.saml_cache: config.saml_cache = saml_xml # The amazon_client now has the SAML assertion it needed (Either via the # cache or freshly generated). From here, we can get the roles and continue # the rest of the workflow regardless of cache. amazon_client = amazon.Amazon(config, saml_xml) roles = amazon_client.roles # Determine the provider and the role arn (if the the user provided isn't an option) if config.role_arn in roles and not config.ask_role: config.provider = roles[config.role_arn] else: if config.account and config.resolve_aliases: aliases = amazon_client.resolve_aws_aliases(roles) config.role_arn, config.provider = util.Util.pick_a_role(roles, aliases, config.account) elif config.account: config.role_arn, config.provider = util.Util.pick_a_role(roles, account=config.account) elif config.resolve_aliases: aliases = amazon_client.resolve_aws_aliases(roles) config.role_arn, config.provider = util.Util.pick_a_role(roles, aliases) else: config.role_arn, config.provider = util.Util.pick_a_role(roles) if not config.quiet: print("Assuming " + config.role_arn) print("Credentials Expiration: " + format(amazon_client.expiration.astimezone(get_localzone()))) if config.print_creds: amazon_client.print_export_line() if config.profile: config.write(amazon_client) def main(): cli_args = sys.argv[1:] cli(cli_args) if __name__ == '__main__': main() ================================================ FILE: aws_google_auth/_version.py ================================================ __version__ = "0.0.38" ================================================ FILE: aws_google_auth/amazon.py ================================================ #!/usr/bin/env python import base64 import boto3 import os import re from datetime import datetime from threading import Thread from botocore.exceptions import ClientError, ProfileNotFound from lxml import etree from aws_google_auth.google import ExpectedGoogleException class Amazon: def __init__(self, config, saml_xml): self.config = config self.saml_xml = saml_xml self.__token = None @property def sts_client(self): try: profile = os.environ.get('AWS_PROFILE') if profile is not None: del os.environ['AWS_PROFILE'] client = boto3.client('sts', region_name=self.config.region) if profile is not None: os.environ['AWS_PROFILE'] = profile return client except ProfileNotFound as ex: raise ExpectedGoogleException("Error : {}.".format(ex)) @property def base64_encoded_saml(self): return base64.b64encode(self.saml_xml).decode("utf-8") @property def token(self): if self.__token is None: self.__token = self.assume_role(self.config.role_arn, self.config.provider, self.base64_encoded_saml, self.config.duration) return self.__token @property def access_key_id(self): return self.token['Credentials']['AccessKeyId'] @property def secret_access_key(self): return self.token['Credentials']['SecretAccessKey'] @property def session_token(self): return self.token['Credentials']['SessionToken'] @property def expiration(self): return self.token['Credentials']['Expiration'] def print_export_line(self): export_template = "export AWS_ACCESS_KEY_ID='{}' AWS_SECRET_ACCESS_KEY='{}' AWS_SESSION_TOKEN='{}' AWS_SESSION_EXPIRATION='{}'" formatted = export_template.format( self.access_key_id, self.secret_access_key, self.session_token, self.expiration.strftime('%Y-%m-%dT%H:%M:%S%z')) print(formatted) @property def roles(self): doc = etree.fromstring(self.saml_xml) roles = {} for x in doc.xpath('//*[@Name = "https://aws.amazon.com/SAML/Attributes/Role"]//text()'): if "arn:aws:iam:" in x or "arn:aws-us-gov:iam:" in x: res = x.split(',') roles[res[0]] = res[1] return roles def assume_role(self, role, principal, saml_assertion, duration=None, auto_duration=True): sts_call_vars = { 'RoleArn': role, 'PrincipalArn': principal, 'SAMLAssertion': saml_assertion } # Try the maximum duration of 12 hours, if it fails try to use the # maximum duration indicated by the error if self.config.auto_duration and auto_duration: sts_call_vars['DurationSeconds'] = self.config.max_duration try: res = self.sts_client.assume_role_with_saml(**sts_call_vars) except ClientError as err: if (err.response.get('Error', []).get('Code') == 'ValidationError' and err.response.get('Error', []).get('Message')): m = re.search( 'Member must have value less than or equal to ([0-9]{3,5})', err.response['Error']['Message'] ) if m is not None and m.group(1): new_duration = int(m.group(1)) return self.assume_role(role, principal, saml_assertion, duration=new_duration, auto_duration=False) # Unknown error or no max time returned in error message raise elif duration: sts_call_vars['DurationSeconds'] = duration res = self.sts_client.assume_role_with_saml(**sts_call_vars) return res def resolve_aws_aliases(self, roles): def resolve_aws_alias(role, principal, aws_dict): session = boto3.session.Session(region_name=self.config.region) sts = session.client('sts') saml = sts.assume_role_with_saml(RoleArn=role, PrincipalArn=principal, SAMLAssertion=self.base64_encoded_saml) iam = session.client('iam', aws_access_key_id=saml['Credentials']['AccessKeyId'], aws_secret_access_key=saml['Credentials']['SecretAccessKey'], aws_session_token=saml['Credentials']['SessionToken']) try: response = iam.list_account_aliases() account_alias = response['AccountAliases'][0] aws_dict[role.split(':')[4]] = account_alias except: sts = session.client('sts', aws_access_key_id=saml['Credentials']['AccessKeyId'], aws_secret_access_key=saml['Credentials']['SecretAccessKey'], aws_session_token=saml['Credentials']['SessionToken']) account_id = sts.get_caller_identity().get('Account') aws_dict[role.split(':')[4]] = '{}'.format(account_id) threads = [] aws_id_alias = {} for number, (role, principal) in enumerate(roles.items()): t = Thread(target=resolve_aws_alias, args=(role, principal, aws_id_alias)) t.start() threads.append(t) for t in threads: t.join() return aws_id_alias @staticmethod def is_valid_saml_assertion(saml_xml): if saml_xml is None: return False try: doc = etree.fromstring(saml_xml) conditions = list(doc.iter(tag='{urn:oasis:names:tc:SAML:2.0:assertion}Conditions')) not_before_str = conditions[0].get('NotBefore') not_on_or_after_str = conditions[0].get('NotOnOrAfter') now = datetime.utcnow() not_before = datetime.strptime(not_before_str, "%Y-%m-%dT%H:%M:%S.%fZ") not_on_or_after = datetime.strptime(not_on_or_after_str, "%Y-%m-%dT%H:%M:%S.%fZ") if not_before <= now < not_on_or_after: return True else: return False except Exception: return False ================================================ FILE: aws_google_auth/configuration.py ================================================ #!/usr/bin/env python import os import botocore.session import filelock try: from backports import configparser except ImportError: import configparser from aws_google_auth import util from aws_google_auth import amazon class Configuration(object): def __init__(self, **kwargs): self.options = {} self.__boto_session = botocore.session.Session() # Set up some defaults. These can be overridden as fit. self.ask_role = False self.keyring = False self.duration = self.max_duration self.auto_duration = False self.idp_id = None self.password = None self.profile = "sts" self.region = None self.role_arn = None self.__saml_cache = None self.sp_id = None self.u2f_disabled = False self.resolve_aliases = False self.username = None self.print_creds = False self.quiet = False self.bg_response = None self.account = "" # For the "~/.aws/config" file, we use the format "[profile testing]" # for the 'testing' profile. The credential file will just be "[testing]" # in that case. See https://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html # for more information. @staticmethod def config_profile(profile): if str(profile).lower() == 'default': return profile else: return 'profile {}'.format(str(profile)) @property def max_duration(self): return 43200 @property def credentials_file(self): return os.path.expanduser(self.__boto_session.get_config_variable('credentials_file')) @property def config_file(self): return os.path.expanduser(self.__boto_session.get_config_variable('config_file')) @property def saml_cache_file(self): return self.credentials_file.replace('credentials', 'saml_cache_%s.xml' % self.idp_id) def ensure_config_files_exist(self): for file in [self.config_file, self.credentials_file]: directory = os.path.dirname(file) if not os.path.exists(directory): os.mkdir(directory, 0o700) if not os.path.exists(file): util.Util.touch(file) # Will return a SAML cache, ONLY if it's valid. If invalid or not set, will # return None. If the SAML cache isn't valid, we'll remove it from the # in-memory object. On the next write(), it will be purged from disk. @property def saml_cache(self): if not amazon.Amazon.is_valid_saml_assertion(self.__saml_cache): self.__saml_cache = None return self.__saml_cache @saml_cache.setter def saml_cache(self, value): self.__saml_cache = value # Will raise exceptions if the configuration is invalid, otherwise returns # None. Use this at any point to validate the configuration is in a good # state. There are no checks here regarding SAML caching, as that's just a # user-performance improvement, and an invalid cache isn't an invalid # configuration. def raise_if_invalid(self): # ask_role assert (self.ask_role.__class__ is bool), "Expected ask_role to be a boolean. Got {}.".format(self.ask_role.__class__) # keyring assert (self.keyring.__class__ is bool), "Expected keyring to be a boolean. Got {}.".format(self.keyring.__class__) # duration assert (self.duration.__class__ is int), "Expected duration to be an integer. Got {}.".format(self.duration.__class__) assert (self.duration >= 900), "Expected duration to be greater than or equal to 900. Got {}.".format(self.duration) assert (self.duration <= self.max_duration), "Expected duration to be less than or equal to max_duration ({}). Got {}.".format(self.max_duration, self.duration) # auto_duration assert (self.auto_duration.__class__ is bool), "Expected auto_duration to be a boolean. Got {}.".format(self.auto_duration.__class__) # profile assert (self.profile.__class__ is str), "Expected profile to be a string. Got {}.".format(self.profile.__class__) # region assert (self.region.__class__ is str), "Expected region to be a string. Got {}.".format(self.region.__class__) # idp_id assert (self.idp_id is not None), "Expected idp_id to be set to non-None value." # sp_id assert (self.sp_id is not None), "Expected sp_id to be set to non-None value." # username assert (self.username.__class__ is str), "Expected username to be a string. Got {}.".format(self.username.__class__) # password try: assert (type(self.password) in [str, unicode]), "Expected password to be a string. Got {}.".format( type(self.password)) except NameError: assert (type(self.password) is str), "Expected password to be a string. Got {}.".format( type(self.password)) # role_arn (Can be blank, we'll just prompt) if self.role_arn is not None: assert (self.role_arn.__class__ is str), "Expected role_arn to be None or a string. Got {}.".format(self.role_arn.__class__) assert ("arn:aws:iam::" in self.role_arn or "arn:aws-us-gov:iam::" in self.role_arn), "Expected role_arn to contain 'arn:aws:iam::'. Got '{}'.".format(self.role_arn) # u2f_disabled assert (self.u2f_disabled.__class__ is bool), "Expected u2f_disabled to be a boolean. Got {}.".format(self.u2f_disabled.__class__) # quiet assert (self.quiet.__class__ is bool), "Expected quiet to be a boolean. Got {}.".format(self.quiet.__class__) # account assert (self.account.__class__ is str), "Expected account to be string. Got {}".format(self.account.__class__) # Write the configuration (and credentials) out to disk. This allows for # regular AWS tooling (aws cli and boto) to use the credentials in the # profile the user specified. def write(self, amazon_object): self.ensure_config_files_exist() assert (self.profile is not None), "Can not store config/credentials if the AWS_PROFILE is None." config_file_lock = filelock.FileLock(self.config_file + '.lock') config_file_lock.acquire() try: # Write to the configuration file profile = Configuration.config_profile(self.profile) config_parser = configparser.RawConfigParser() config_parser.read(self.config_file) if not config_parser.has_section(profile): config_parser.add_section(profile) config_parser.set(profile, 'region', self.region) config_parser.set(profile, 'google_config.ask_role', self.ask_role) config_parser.set(profile, 'google_config.keyring', self.keyring) config_parser.set(profile, 'google_config.duration', self.duration) config_parser.set(profile, 'google_config.google_idp_id', self.idp_id) config_parser.set(profile, 'google_config.role_arn', self.role_arn) config_parser.set(profile, 'google_config.google_sp_id', self.sp_id) config_parser.set(profile, 'google_config.u2f_disabled', self.u2f_disabled) config_parser.set(profile, 'google_config.google_username', self.username) config_parser.set(profile, 'google_config.bg_response', self.bg_response) with open(self.config_file, 'w+') as f: config_parser.write(f) finally: config_file_lock.release() # Write to the credentials file (only if we have credentials) if amazon_object is not None: credentials_file_lock = filelock.FileLock(self.credentials_file + '.lock') credentials_file_lock.acquire() try: credentials_parser = configparser.RawConfigParser() credentials_parser.read(self.credentials_file) if not credentials_parser.has_section(self.profile): credentials_parser.add_section(self.profile) credentials_parser.set(self.profile, 'aws_access_key_id', amazon_object.access_key_id) credentials_parser.set(self.profile, 'aws_secret_access_key', amazon_object.secret_access_key) credentials_parser.set(self.profile, 'aws_security_token', amazon_object.session_token) credentials_parser.set(self.profile, 'aws_session_expiration', amazon_object.expiration.strftime('%Y-%m-%dT%H:%M:%S%z')) credentials_parser.set(self.profile, 'aws_session_token', amazon_object.session_token) with open(self.credentials_file, 'w+') as f: credentials_parser.write(f) finally: credentials_file_lock.release() if self.__saml_cache is not None: saml_cache_file_lock = filelock.FileLock(self.saml_cache_file + '.lock') saml_cache_file_lock.acquire() try: with open(self.saml_cache_file, 'w') as f: f.write(self.__saml_cache.decode("utf-8")) finally: saml_cache_file_lock.release() # Read from the configuration file and override ALL values currently stored # in the configuration object. As this is potentially destructive, it's # important to only run this in the beginning of the object initialization. # We do not read AWS credentials, as this tool's use case is to obtain # them. def read(self, profile): self.ensure_config_files_exist() # Shortening Convenience functions coalesce = util.Util.coalesce unicode_to_string = util.Util.unicode_to_string_if_needed profile_string = Configuration.config_profile(profile) config_parser = configparser.RawConfigParser() config_parser.read(self.config_file) if config_parser.has_section(profile_string): self.profile = profile # Ask Role read_ask_role = config_parser[profile_string].getboolean('google_config.ask_role', None) self.ask_role = coalesce(read_ask_role, self.ask_role) # Keyring read_keyring = config_parser[profile_string].getboolean('google_config.keyring', None) self.keyring = coalesce(read_keyring, self.keyring) # Duration read_duration = config_parser[profile_string].getint('google_config.duration', None) self.duration = coalesce(read_duration, self.duration) # IDP ID read_idp_id = unicode_to_string(config_parser[profile_string].get('google_config.google_idp_id', None)) self.idp_id = coalesce(read_idp_id, self.idp_id) # Region read_region = unicode_to_string(config_parser[profile_string].get('region', None)) self.region = coalesce(read_region, self.region) # Role ARN read_role_arn = unicode_to_string(config_parser[profile_string].get('google_config.role_arn', None)) self.role_arn = coalesce(read_role_arn, self.role_arn) # SP ID read_sp_id = unicode_to_string(config_parser[profile_string].get('google_config.google_sp_id', None)) self.sp_id = coalesce(read_sp_id, self.sp_id) # U2F Disabled read_u2f_disabled = config_parser[profile_string].getboolean('google_config.u2f_disabled', None) self.u2f_disabled = coalesce(read_u2f_disabled, self.u2f_disabled) # Username read_username = unicode_to_string(config_parser[profile_string].get('google_config.google_username', None)) self.username = coalesce(read_username, self.username) # bg_response read_bg_response = unicode_to_string(config_parser[profile_string].get('google_config.bg_response', None)) self.bg_response = coalesce(read_bg_response, self.bg_response) # Account read_account = unicode_to_string(config_parser[profile_string].get('account', None)) self.account = coalesce(read_account, self.account) # SAML Cache try: with open(self.saml_cache_file, 'r') as f: self.__saml_cache = f.read().encode("utf-8") except IOError: pass ================================================ FILE: aws_google_auth/google.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import print_function import base64 import io import json import logging import os import re import sys import requests from PIL import Image from datetime import datetime from distutils.spawn import find_executable from bs4 import BeautifulSoup from requests import HTTPError from six import print_ as print from six.moves import urllib_parse, input from aws_google_auth import _version # The U2F USB Library is optional, if it's there, include it. try: from aws_google_auth import u2f except ImportError: logging.info("Failed to import U2F libraries, U2F login unavailable. " "Other methods can still continue.") class ExpectedGoogleException(Exception): def __init__(self, *args): super(ExpectedGoogleException, self).__init__(*args) class Google: def __init__(self, config, save_failure, save_flow=False): """The Google object holds authentication state for a given session. You need to supply: username: FQDN Google username, eg first.last@example.com password: obvious idp_id: Google's assigned IdP identifier for your G-suite account sp_id: Google's assigned SP identifier for your AWS SAML app Optionally, you can supply: duration_seconds: number of seconds for the session to be active (max 43200) """ self.version = _version.__version__ self.config = config self.base_url = 'https://accounts.google.com' self.save_failure = save_failure self.session_state = None self.save_flow = save_flow if save_flow: self.save_flow_dict = {} self.save_flow_dir = "aws-google-auth-" + datetime.now().strftime('%Y-%m-%dT%H%M%S') os.makedirs(self.save_flow_dir, exist_ok=True) @property def login_url(self): return self.base_url + "/o/saml2/initsso?idpid={}&spid={}&forceauthn=false".format( self.config.idp_id, self.config.sp_id) def check_for_failure(self, sess): if isinstance(sess.reason, bytes): # We attempt to decode utf-8 first because some servers # choose to localize their reason strings. If the string # isn't utf-8, we fall back to iso-8859-1 for all other # encodings. (See PR #3538) try: reason = sess.reason.decode('utf-8') except UnicodeDecodeError: reason = sess.reason.decode('iso-8859-1') else: reason = sess.reason if sess.status_code == 403: raise ExpectedGoogleException(u'{} accessing {}'.format( reason, sess.url)) try: sess.raise_for_status() except HTTPError as ex: if self.save_failure: logging.exception("Saving failure trace in 'failure.html'", ex) with open("failure.html", 'w') as out: out.write(sess.text) raise ex return sess def _save_file_name(self, url): filename = url.split('://')[1].split('?')[0].replace("accounts.google", "ac.go").replace("/", "~") file_idx = self.save_flow_dict.get(filename, 1) self.save_flow_dict[filename] = file_idx + 1 return filename + "_" + str(file_idx) def _save_request(self, url, method='GET', data=None, json_data=None): if self.save_flow: filename = self._save_file_name(url) + "_" + method + ".req" with open(os.path.join(self.save_flow_dir, filename), 'w', encoding='utf-8') as out: try: out.write("params=" + url.split('?')[1]) except IndexError: out.write("params=None") out.write(("\ndata: " + json.dumps(data, indent=2)).replace(self.config.password, '')) out.write(("\njson: " + json.dumps(json_data, indent=2)).replace(self.config.password, '')) def _save_response(self, url, response): if self.save_flow: filename = self._save_file_name(url) + ".html" with open(os.path.join(self.save_flow_dir, filename), 'w', encoding='utf-8') as out: out.write(response.text) def post(self, url, data=None, json_data=None): try: self._save_request(url, method='POST', data=data, json_data=json_data) response = self.check_for_failure(self.session.post(url, data=data, json=json_data)) self._save_response(url, response) except requests.exceptions.ConnectionError as e: logging.exception( 'There was a connection error, check your network settings.', e) sys.exit(1) except requests.exceptions.Timeout as e: logging.exception('The connection timed out, please try again.', e) sys.exit(1) except requests.exceptions.TooManyRedirects as e: logging.exception('The number of redirects exceeded the maximum ' 'allowed.', e) sys.exit(1) return response def get(self, url): try: self._save_request(url) response = self.check_for_failure(self.session.get(url)) self._save_response(url, response) except requests.exceptions.ConnectionError as e: logging.exception( 'There was a connection error, check your network settings.', e) sys.exit(1) except requests.exceptions.Timeout as e: logging.exception('The connection timed out, please try again.', e) sys.exit(1) except requests.exceptions.TooManyRedirects as e: logging.exception('The number of redirects exceeded the maximum ' 'allowed.', e) sys.exit(1) return response @staticmethod def parse_error_message(sess): response_page = BeautifulSoup(sess.text, 'html.parser') error = response_page.find('span', {'id': 'errorMsg'}) if error is None: return None else: return error.text @staticmethod def find_key_handles(input, challengeTxt): keyHandles = [] typeOfInput = type(input) if typeOfInput == dict: # parse down a dict for item in input: keyHandles.extend(Google.find_key_handles(input[item], challengeTxt)) elif typeOfInput == list: # looks like we've hit an array - iterate it array = list(filter(None, input)) # remove any None type objects from the array for item in array: typeValue = type(item) if typeValue == list: # another array - recursive call keyHandles.extend(Google.find_key_handles(item, challengeTxt)) elif typeValue == int or typeValue == bool: # ints bools etc we don't care continue else: # we went a string or unicode here (python 3.x lost unicode global) try: # keyHandle string will be base64 encoded - # if its not an exception is thrown and we continue as its not the string we're after base64UrlEncoded = base64.urlsafe_b64encode(base64.b64decode(item)) if base64UrlEncoded != challengeTxt: # make sure its not the challengeTxt - if it not return it keyHandles.append(base64UrlEncoded) except: pass return keyHandles @staticmethod def find_app_id(inputString): try: searchResult = re.search('"appid":"[a-z://.-_] + "', inputString).group() searchObject = json.loads('{' + searchResult + '}') return str(searchObject['appid']) except: logging.exception('Was unable to find appid value in googles SAML page') sys.exit(1) def do_login(self): self.session = requests.Session() self.session.headers['User-Agent'] = "AWS Sign-in/{} (aws-google-auth)".format(self.version) sess = self.get(self.login_url) # Collect information from the page source first_page = BeautifulSoup(sess.text, 'html.parser') # gxf = first_page.find('input', {'name': 'gxf'}).get('value') self.cont = first_page.find('input', {'name': 'continue'}).get('value') # page = first_page.find('input', {'name': 'Page'}).get('value') # sign_in = first_page.find('input', {'name': 'signIn'}).get('value') form = first_page.find('form', {'id': 'gaia_loginform'}) account_login_url = form.get('action') payload = {} for tag in form.find_all('input'): if tag.get('name') is None: continue payload[tag.get('name')] = tag.get('value') payload['Email'] = self.config.username if self.config.bg_response: payload['bgresponse'] = self.config.bg_response if payload.get('PersistentCookie', None) is not None: payload['PersistentCookie'] = 'yes' if payload.get('TrustDevice', None) is not None: payload['TrustDevice'] = 'on' # POST to account login info page, to collect profile and session info sess = self.post(account_login_url, data=payload) self.session.headers['Referer'] = sess.url # Collect ProfileInformation, SessionState, signIn, and Password Challenge URL challenge_page = BeautifulSoup(sess.text, 'html.parser') # Handle the "old-style" page if challenge_page.find('form', {'id': 'gaia_loginform'}): form = challenge_page.find('form', {'id': 'gaia_loginform'}) passwd_challenge_url = form.get('action') else: # sometimes they serve up a different page logging.info("Handling new-style login page") form = challenge_page.find('form', {'id': 'challenge'}) passwd_challenge_url = 'https://accounts.google.com' + form.get('action') for tag in form.find_all('input'): if tag.get('name') is None: continue payload[tag.get('name')] = tag.get('value') # Update the payload payload['Passwd'] = self.config.password # Set bg_response in request payload to passwd challenge if self.config.bg_response: payload['bgresponse'] = self.config.bg_response # POST to Authenticate Password sess = self.post(passwd_challenge_url, data=payload) response_page = BeautifulSoup(sess.text, 'html.parser') error = response_page.find(class_='error-msg') cap = response_page.find('input', {'name': 'identifier-captcha-input'}) # Were there any errors logging in? Could be invalid username or password # There could also sometimes be a Captcha, which means Google thinks you, # or someone using the same outbound IP address as you, is a bot. if error is not None and cap is None: raise ExpectedGoogleException('Invalid username or password') if "signin/rejected" in sess.url: raise ExpectedGoogleException(u'''Default value of parameter `bgresponse` has not accepted. Please visit login URL {}, open the web inspector and execute document.bg.invoke() in the console. Then, set --bg-response to the function output.'''.format(self.login_url)) self.check_extra_step(response_page) # Process Google CAPTCHA verification request if present if cap is not None: self.session.headers['Referer'] = sess.url sess = self.handle_captcha(sess, payload) response_page = BeautifulSoup(sess.text, 'html.parser') error = response_page.find(class_='error-msg') cap = response_page.find('input', {'name': 'logincaptcha'}) # Were there any errors logging in? Could be invalid username or password # There could also sometimes be a Captcha, which means Google thinks you, # or someone using the same outbound IP address as you, is a bot. if error is not None: raise ExpectedGoogleException('Invalid username or password') self.check_extra_step(response_page) if cap is not None: raise ExpectedGoogleException( 'Invalid captcha') self.session.headers['Referer'] = sess.url if "selectchallenge/" in sess.url: sess = self.handle_selectchallenge(sess) # Was there an MFA challenge? if "challenge/totp/" in sess.url: error_msg = "" while error_msg is not None: sess = self.handle_totp(sess) error_msg = self.parse_error_message(sess) if error_msg is not None: logging.error(error_msg) elif "challenge/ipp/" in sess.url: sess = self.handle_sms(sess) elif "challenge/az/" in sess.url: sess = self.handle_prompt(sess) elif "challenge/sk/" in sess.url: sess = self.handle_sk(sess) elif "challenge/iap/" in sess.url: sess = self.handle_iap(sess) elif "challenge/dp/" in sess.url: sess = self.handle_dp(sess) elif "challenge/ootp/5" in sess.url: raise NotImplementedError( 'Offline Google App OOTP not implemented') # ... there are different URLs for backup codes (printed) # and security keys (eg yubikey) as well # save for later self.session_state = sess @staticmethod def check_extra_step(response): extra_step = response.find(text='This extra step shows that it’s really you trying to sign in') if extra_step: if response.find(id='contactAdminMessage'): raise ValueError(response.find(id='contactAdminMessage').text) def parse_saml(self): if self.session_state is None: raise RuntimeError('You must use do_login() before calling parse_saml()') parsed = BeautifulSoup(self.session_state.text, 'html.parser') try: saml_element = parsed.find('input', {'name': 'SAMLResponse'}).get('value') except: if self.save_failure: logging.error("SAML lookup failed, storing failure page to " "'saml.html' to assist with debugging.") with open("saml.html", 'wb') as out: out.write(self.session_state.text.encode('utf-8')) raise ExpectedGoogleException('Something went wrong - Could not find SAML response, check your credentials or use --save-failure-html to debug.') return base64.b64decode(saml_element) def handle_captcha(self, sess, payload): response_page = BeautifulSoup(sess.text, 'html.parser') # Collect ProfileInformation, SessionState, signIn, and Password Challenge URL profile_information = response_page.find('input', { 'name': 'ProfileInformation' }).get('value') session_state = response_page.find('input', { 'name': 'SessionState' }).get('value') sign_in = response_page.find('input', {'name': 'signIn'}).get('value') passwd_challenge_url = response_page.find('form', { 'id': 'gaia_loginform' }).get('action') # Update the payload payload['SessionState'] = session_state payload['ProfileInformation'] = profile_information payload['signIn'] = sign_in payload['Passwd'] = self.config.password # Get all captcha challenge tokens and urls captcha_container = response_page.find('div', {'id': 'identifier-captcha'}) captcha_logintoken = captcha_container.find('input', {'id': 'identifier-token'}).get('value') captcha_img = captcha_container.find('div', {'class': 'captcha-img'}) captcha_url = "https://accounts.google.com" + captcha_img.find('img').get('src') captcha_logintoken_audio = '' open_image = True # Check if there is a display utility installed as Image.open(f).show() do not raise any exception if not # if neither xv or display are available just display the URL for the user to visit. if os.name == 'posix' and sys.platform != 'darwin': if find_executable('xv') is None and find_executable('display') is None: open_image = False print("Please visit the following URL to view your CAPTCHA: {}".format(captcha_url)) if open_image: try: with requests.get(captcha_url) as url: with io.BytesIO(url.content) as f: Image.open(f).show() except Exception: pass try: captcha_input = raw_input("Captcha (case insensitive): ") or None except NameError: captcha_input = input("Captcha (case insensitive): ") or None # Update the payload payload['identifier-captcha-input'] = captcha_input payload['identifiertoken'] = captcha_logintoken payload['identifiertoken_audio'] = captcha_logintoken_audio payload['checkedDomains'] = 'youtube' payload['checkConnection'] = 'youtube:574:1' payload['Email'] = self.config.username response = self.post(passwd_challenge_url, data=payload) newPayload = {} auth_response_page = BeautifulSoup(response.text, 'html.parser') form = auth_response_page.find('form') for tag in form.find_all('input'): if tag.get('name') is None: continue newPayload[tag.get('name')] = tag.get('value') newPayload['Email'] = self.config.username newPayload['Passwd'] = self.config.password if newPayload.get('TrustDevice', None) is not None: newPayload['TrustDevice'] = 'on' return self.post(response.url, data=newPayload) def handle_sk(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') challenge_url = sess.url.split("?")[0] challenges_txt = response_page.find('input', { 'name': "id-challenge" }).get('value') facet_url = urllib_parse.urlparse(challenge_url) facet = facet_url.scheme + "://" + facet_url.netloc keyHandleJSField = response_page.find('div', {'jsname': 'C0oDBd'}).get('data-challenge-ui') startJSONPosition = keyHandleJSField.find('{') endJSONPosition = keyHandleJSField.rfind('}') keyHandleJsonPayload = json.loads(keyHandleJSField[startJSONPosition:endJSONPosition + 1]) keyHandles = self.find_key_handles(keyHandleJsonPayload, base64.urlsafe_b64encode(base64.b64decode(challenges_txt))) appId = self.find_app_id(str(keyHandleJsonPayload)) # txt sent for signing needs to be base64 url encode # we also have to remove any base64 padding because including including it will prevent google accepting the auth response challenges_txt_encode_pad_removed = base64.urlsafe_b64encode(base64.b64decode(challenges_txt)).strip('='.encode()) u2f_challenges = [{'version': 'U2F_V2', 'challenge': challenges_txt_encode_pad_removed.decode(), 'appId': appId, 'keyHandle': keyHandle.decode()} for keyHandle in keyHandles] # Prompt the user up to attempts_remaining times to insert their U2F device. attempts_remaining = 5 auth_response = None while True: try: auth_response_dict = u2f.u2f_auth(u2f_challenges, facet) auth_response = json.dumps(auth_response_dict) break except RuntimeWarning: logging.error("No U2F device found. %d attempts remaining", attempts_remaining) if attempts_remaining <= 0: break else: input( "Insert your U2F device and press enter to try again..." ) attempts_remaining -= 1 # If we exceed the number of attempts, raise an error and let the program exit. if auth_response is None: raise ExpectedGoogleException( "No U2F device found. Please check your setup.") payload = { 'challengeId': response_page.find('input', { 'name': 'challengeId' }).get('value'), 'challengeType': response_page.find('input', { 'name': 'challengeType' }).get('value'), 'continue': response_page.find('input', { 'name': 'continue' }).get('value'), 'scc': response_page.find('input', { 'name': 'scc' }).get('value'), 'sarp': response_page.find('input', { 'name': 'sarp' }).get('value'), 'checkedDomains': response_page.find('input', { 'name': 'checkedDomains' }).get('value'), 'pstMsg': '1', 'TL': response_page.find('input', { 'name': 'TL' }).get('value'), 'gxf': response_page.find('input', { 'name': 'gxf' }).get('value'), 'id-challenge': challenges_txt, 'id-assertion': auth_response, 'TrustDevice': 'on', } return self.post(challenge_url, data=payload) def handle_sms(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') challenge_url = sess.url.split("?")[0] sms_token = input("Enter SMS token: G-") or None challenge_form = response_page.find('form') payload = {} for tag in challenge_form.find_all('input'): if tag.get('name') is None: continue payload[tag.get('name')] = tag.get('value') if response_page.find('input', {'name': 'TrustDevice'}) is not None: payload['TrustDevice'] = 'on' payload['Pin'] = sms_token try: del payload['SendMethod'] except KeyError: pass # Submit IPP (SMS code) return self.post(challenge_url, data=payload) def handle_prompt(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') challenge_url = sess.url.split("?")[0] data_key = response_page.find('div', { 'data-api-key': True }).get('data-api-key') data_tx_id = response_page.find('div', { 'data-tx-id': True }).get('data-tx-id') # Need to post this to the verification/pause endpoint await_url = "https://content.googleapis.com/cryptauth/v1/authzen/awaittx?alt=json&key={}".format( data_key) await_body = {'txId': data_tx_id} self.check_prompt_code(response_page) print("Open the Google App, and tap 'Yes' on the prompt to sign in ...") self.session.headers['Referer'] = sess.url retry = True response = None while retry: try: response = self.post(await_url, json_data=await_body) retry = False except requests.exceptions.HTTPError as ex: if not ex.response.status_code == 500: raise ex parsed_response = json.loads(response.text) payload = { 'challengeId': response_page.find('input', { 'name': 'challengeId' }).get('value'), 'challengeType': response_page.find('input', { 'name': 'challengeType' }).get('value'), 'continue': response_page.find('input', { 'name': 'continue' }).get('value'), 'scc': response_page.find('input', { 'name': 'scc' }).get('value'), 'sarp': response_page.find('input', { 'name': 'sarp' }).get('value'), 'checkedDomains': response_page.find('input', { 'name': 'checkedDomains' }).get('value'), 'checkConnection': 'youtube:1295:1', 'pstMsg': response_page.find('input', { 'name': 'pstMsg' }).get('value'), 'TL': response_page.find('input', { 'name': 'TL' }).get('value'), 'gxf': response_page.find('input', { 'name': 'gxf' }).get('value'), 'token': parsed_response['txToken'], 'action': response_page.find('input', { 'name': 'action' }).get('value'), 'TrustDevice': 'on', } return self.post(challenge_url, data=payload) @staticmethod def check_prompt_code(response): """ Sometimes there is an additional numerical code on the response page that needs to be selected on the prompt from a list of multiple choice. Print it if it's there. """ num_code = response.find("div", {"jsname": "EKvSSd"}) if num_code: print("numerical code for prompt: {}".format(num_code.string)) def handle_totp(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') tl = response_page.find('input', {'name': 'TL'}).get('value') gxf = response_page.find('input', {'name': 'gxf'}).get('value') challenge_url = sess.url.split("?")[0] challenge_id = challenge_url.split("totp/")[1] mfa_token = input("MFA token: ") or None if not mfa_token: raise ValueError( "MFA token required for {} but none supplied.".format( self.config.username)) payload = { 'challengeId': challenge_id, 'challengeType': 6, 'continue': self.cont, 'scc': 1, 'sarp': 1, 'checkedDomains': 'youtube', 'pstMsg': 0, 'TL': tl, 'gxf': gxf, 'Pin': mfa_token, 'TrustDevice': 'on', } # Submit TOTP return self.post(challenge_url, data=payload) def handle_dp(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') input("Check your phone - after you have confirmed response press ENTER to continue.") or None form = response_page.find('form', {'id': 'challenge'}) challenge_url = 'https://accounts.google.com' + form.get('action') payload = {} for tag in form.find_all('input'): if tag.get('name') is None: continue payload[tag.get('name')] = tag.get('value') # Submit Configuration return self.post(challenge_url, data=payload) def handle_iap(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') challenge_url = sess.url.split("?")[0] phone_number = input('Enter your phone number:') or None while True: try: choice = int( input( 'Type 1 to receive a code by SMS or 2 for a voice call:' )) if choice not in [1, 2]: raise ValueError except ValueError: logging.error("Not a valid (integer) option, try again") continue else: if choice == 1: send_method = 'SMS' elif choice == 2: send_method = 'VOICE' else: continue break payload = { 'challengeId': response_page.find('input', { 'name': 'challengeId' }).get('value'), 'challengeType': response_page.find('input', { 'name': 'challengeType' }).get('value'), 'continue': self.cont, 'scc': response_page.find('input', { 'name': 'scc' }).get('value'), 'sarp': response_page.find('input', { 'name': 'sarp' }).get('value'), 'checkedDomains': response_page.find('input', { 'name': 'checkedDomains' }).get('value'), 'pstMsg': response_page.find('input', { 'name': 'pstMsg' }).get('value'), 'TL': response_page.find('input', { 'name': 'TL' }).get('value'), 'gxf': response_page.find('input', { 'name': 'gxf' }).get('value'), 'phoneNumber': phone_number, 'sendMethod': send_method, } # Submit phone number and desired method (SMS or voice call) sess = self.post(challenge_url, data=payload) response_page = BeautifulSoup(sess.text, 'html.parser') challenge_url = sess.url.split("?")[0] token = input("Enter " + send_method + " token: G-") or None payload = { 'challengeId': response_page.find('input', { 'name': 'challengeId' }).get('value'), 'challengeType': response_page.find('input', { 'name': 'challengeType' }).get('value'), 'continue': response_page.find('input', { 'name': 'continue' }).get('value'), 'scc': response_page.find('input', { 'name': 'scc' }).get('value'), 'sarp': response_page.find('input', { 'name': 'sarp' }).get('value'), 'checkedDomains': response_page.find('input', { 'name': 'checkedDomains' }).get('value'), 'pstMsg': response_page.find('input', { 'name': 'pstMsg' }).get('value'), 'TL': response_page.find('input', { 'name': 'TL' }).get('value'), 'gxf': response_page.find('input', { 'name': 'gxf' }).get('value'), 'pin': token, } # Submit SMS/VOICE token return self.post(challenge_url, data=payload) def handle_selectchallenge(self, sess): response_page = BeautifulSoup(sess.text, 'html.parser') challenges = [] for i in response_page.select('form[data-challengeentry]'): action = i.attrs.get("action") if "challenge/totp/" in action: challenges.append(['TOTP (Google Authenticator)', i.attrs.get("data-challengeentry")]) elif "challenge/ipp/" in action: challenges.append(['SMS', i.attrs.get("data-challengeentry")]) elif "challenge/iap/" in action: challenges.append(['SMS other phone', i.attrs.get("data-challengeentry")]) elif "challenge/sk/" in action: challenges.append(['YubiKey', i.attrs.get("data-challengeentry")]) elif "challenge/az/" in action: challenges.append(['Google Prompt', i.attrs.get("data-challengeentry")]) print('Choose MFA method from available:') for i, mfa in enumerate(challenges, start=1): print("{}: {}".format(i, mfa[0])) selected_challenge = input("Enter MFA choice number (1): ") or None if selected_challenge is not None and int(selected_challenge) <= len(challenges): selected_challenge = int(selected_challenge) - 1 else: selected_challenge = 0 challenge_id = challenges[selected_challenge][1] print("MFA Type Chosen: {}".format(challenges[selected_challenge][0])) # We need the specific form of the challenge chosen challenge_form = response_page.find( 'form', {'data-challengeentry': challenge_id}) payload = {} for tag in challenge_form.find_all('input'): if tag.get('name') is None: continue payload[tag.get('name')] = tag.get('value') if response_page.find('input', {'name': 'TrustDevice'}) is not None: payload['TrustDevice'] = 'on' # POST to google with the chosen challenge return self.post( self.base_url + challenge_form.get('action'), data=payload) ================================================ FILE: aws_google_auth/tests/__init__.py ================================================ ================================================ FILE: aws_google_auth/tests/google_error.html ================================================ Google Accounts

2-step Verification

This extra step shows that it’s really you trying to sign in

Based on your organisation's policy, you need to turn on 2-step verification. Contact your administrator to learn more.
Enter a backup code
Enter one of the backup codes you received from Google.
Remember this computer for 30 days
================================================ FILE: aws_google_auth/tests/saml-response-expired-before-valid.xml ================================================ https://accounts.google.com/o/saml2?idpid=abcd12345 https://accounts.google.com/o/saml2?idpid=abcd12345 GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y= dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+ VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0 SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA== ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. MIIDdDCCAlygAwIBAgIGAVXC/OcnMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzA3 MDEzMzE5WhcNMjEwNzA2MDEzMzE5WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAhkv0Sr7ALfc58YrnLXzVGfTRg1T9xUfuZqhdu80BgHTfaJDLX66icHHRRoso/hho EIYo1pUQTq0DtgmqkLg9rAup3rR+pImfcHBC55+vMDoEf5t88H/i0qDn3r63PxeULRoFIkCX9aVG uUPDe2CHAxB1UXUxyDf7ZAdIQJLPJdOQlsNRleBBoek4vuo2ZHv+A2tbAhE8/rIoQlDvXSpCZ9P7 m9TrFOb7tB4pHjJjESdmqcnEFc5zepAT8IuRAGZ1OkjJs74JUp+03do8scTMXzvVi4jefpyXhnoN C0da4OwPig7UmbDsrSCGbqz29UgxmGUmSnLchpkglw1eET5hTwIDAQABMA0GCSqGSIb3DQEBCwUA A4IBAQAA5WBtCPlaSIm1NIpKYd2x8qfeKc2YsxbAPukgUFaRDl1uxGw1HdzNzUp9X4JOF/futpw/ yhmw9o1GHBukIdj0mJRt8O9szRdkJmx4EfbY5bTVzkQ7QGv9FI1LBD6z6KgJEOxEGpDbh2Z8uyW8 HvxXgZgiyan53FauVJe+UuAkBy2ynJcVKK3+vUEISFXn1oh5SPOmi+2R4WKSgyTqOKpuowHHHg9u EbwwnXPMU4q3QLG1oDrp0ZvVuprvJaoWd5zIt/TYB3Hb5oEO7Imwx1n9K9QskYmFygR9rdJ6VS7L 6/h6rcL/dKjm4pU0Dgk9h9Hi8ps7Mn+nRRhsWQbiD59n first.last@example.com https://signin.aws.amazon.com/saml first.last@example.com arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps 28800 urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ================================================ FILE: aws_google_auth/tests/saml-response-no-expire.xml ================================================ https://accounts.google.com/o/saml2?idpid=abcd12345 https://accounts.google.com/o/saml2?idpid=abcd12345 GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y= dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+ VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0 SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA== ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. MIIDdDCCAlygAwIBAgIGAVXC/OcnMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzA3 MDEzMzE5WhcNMjEwNzA2MDEzMzE5WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAhkv0Sr7ALfc58YrnLXzVGfTRg1T9xUfuZqhdu80BgHTfaJDLX66icHHRRoso/hho EIYo1pUQTq0DtgmqkLg9rAup3rR+pImfcHBC55+vMDoEf5t88H/i0qDn3r63PxeULRoFIkCX9aVG uUPDe2CHAxB1UXUxyDf7ZAdIQJLPJdOQlsNRleBBoek4vuo2ZHv+A2tbAhE8/rIoQlDvXSpCZ9P7 m9TrFOb7tB4pHjJjESdmqcnEFc5zepAT8IuRAGZ1OkjJs74JUp+03do8scTMXzvVi4jefpyXhnoN C0da4OwPig7UmbDsrSCGbqz29UgxmGUmSnLchpkglw1eET5hTwIDAQABMA0GCSqGSIb3DQEBCwUA A4IBAQAA5WBtCPlaSIm1NIpKYd2x8qfeKc2YsxbAPukgUFaRDl1uxGw1HdzNzUp9X4JOF/futpw/ yhmw9o1GHBukIdj0mJRt8O9szRdkJmx4EfbY5bTVzkQ7QGv9FI1LBD6z6KgJEOxEGpDbh2Z8uyW8 HvxXgZgiyan53FauVJe+UuAkBy2ynJcVKK3+vUEISFXn1oh5SPOmi+2R4WKSgyTqOKpuowHHHg9u EbwwnXPMU4q3QLG1oDrp0ZvVuprvJaoWd5zIt/TYB3Hb5oEO7Imwx1n9K9QskYmFygR9rdJ6VS7L 6/h6rcL/dKjm4pU0Dgk9h9Hi8ps7Mn+nRRhsWQbiD59n first.last@example.com https://signin.aws.amazon.com/saml first.last@example.com arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps 28800 urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ================================================ FILE: aws_google_auth/tests/saml-response-too-late.xml ================================================ https://accounts.google.com/o/saml2?idpid=abcd12345 https://accounts.google.com/o/saml2?idpid=abcd12345 GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y= dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+ VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0 SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA== ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. MIIDdDCCAlygAwIBAgIGAVXC/OcnMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzA3 MDEzMzE5WhcNMjEwNzA2MDEzMzE5WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAhkv0Sr7ALfc58YrnLXzVGfTRg1T9xUfuZqhdu80BgHTfaJDLX66icHHRRoso/hho EIYo1pUQTq0DtgmqkLg9rAup3rR+pImfcHBC55+vMDoEf5t88H/i0qDn3r63PxeULRoFIkCX9aVG uUPDe2CHAxB1UXUxyDf7ZAdIQJLPJdOQlsNRleBBoek4vuo2ZHv+A2tbAhE8/rIoQlDvXSpCZ9P7 m9TrFOb7tB4pHjJjESdmqcnEFc5zepAT8IuRAGZ1OkjJs74JUp+03do8scTMXzvVi4jefpyXhnoN C0da4OwPig7UmbDsrSCGbqz29UgxmGUmSnLchpkglw1eET5hTwIDAQABMA0GCSqGSIb3DQEBCwUA A4IBAQAA5WBtCPlaSIm1NIpKYd2x8qfeKc2YsxbAPukgUFaRDl1uxGw1HdzNzUp9X4JOF/futpw/ yhmw9o1GHBukIdj0mJRt8O9szRdkJmx4EfbY5bTVzkQ7QGv9FI1LBD6z6KgJEOxEGpDbh2Z8uyW8 HvxXgZgiyan53FauVJe+UuAkBy2ynJcVKK3+vUEISFXn1oh5SPOmi+2R4WKSgyTqOKpuowHHHg9u EbwwnXPMU4q3QLG1oDrp0ZvVuprvJaoWd5zIt/TYB3Hb5oEO7Imwx1n9K9QskYmFygR9rdJ6VS7L 6/h6rcL/dKjm4pU0Dgk9h9Hi8ps7Mn+nRRhsWQbiD59n first.last@example.com https://signin.aws.amazon.com/saml first.last@example.com arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps 28800 urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ================================================ FILE: aws_google_auth/tests/saml-response-too-soon.xml ================================================ https://accounts.google.com/o/saml2?idpid=abcd12345 https://accounts.google.com/o/saml2?idpid=abcd12345 GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y= dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+ VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0 SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA== ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. MIIDdDCCAlygAwIBAgIGAVXC/OcnMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzA3 MDEzMzE5WhcNMjEwNzA2MDEzMzE5WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAhkv0Sr7ALfc58YrnLXzVGfTRg1T9xUfuZqhdu80BgHTfaJDLX66icHHRRoso/hho EIYo1pUQTq0DtgmqkLg9rAup3rR+pImfcHBC55+vMDoEf5t88H/i0qDn3r63PxeULRoFIkCX9aVG uUPDe2CHAxB1UXUxyDf7ZAdIQJLPJdOQlsNRleBBoek4vuo2ZHv+A2tbAhE8/rIoQlDvXSpCZ9P7 m9TrFOb7tB4pHjJjESdmqcnEFc5zepAT8IuRAGZ1OkjJs74JUp+03do8scTMXzvVi4jefpyXhnoN C0da4OwPig7UmbDsrSCGbqz29UgxmGUmSnLchpkglw1eET5hTwIDAQABMA0GCSqGSIb3DQEBCwUA A4IBAQAA5WBtCPlaSIm1NIpKYd2x8qfeKc2YsxbAPukgUFaRDl1uxGw1HdzNzUp9X4JOF/futpw/ yhmw9o1GHBukIdj0mJRt8O9szRdkJmx4EfbY5bTVzkQ7QGv9FI1LBD6z6KgJEOxEGpDbh2Z8uyW8 HvxXgZgiyan53FauVJe+UuAkBy2ynJcVKK3+vUEISFXn1oh5SPOmi+2R4WKSgyTqOKpuowHHHg9u EbwwnXPMU4q3QLG1oDrp0ZvVuprvJaoWd5zIt/TYB3Hb5oEO7Imwx1n9K9QskYmFygR9rdJ6VS7L 6/h6rcL/dKjm4pU0Dgk9h9Hi8ps7Mn+nRRhsWQbiD59n first.last@example.com https://signin.aws.amazon.com/saml first.last@example.com arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps 28800 urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ================================================ FILE: aws_google_auth/tests/test_amazon.py ================================================ #!/usr/bin/env python import unittest import mock from aws_google_auth import amazon from aws_google_auth import configuration from os import path import os class TestAmazon(unittest.TestCase): @property def valid_config(self): return configuration.Configuration( idp_id="IDPID", sp_id="SPID", username="user@example.com", password="hunter2") def read_local_file(self, filename): here = path.abspath(path.dirname(__file__)) with open(path.join(here, filename)) as fp: return fp.read().encode('utf-8') def test_sts_client(self): a = amazon.Amazon(self.valid_config, "dummy-encoded-saml") self.assertEqual(str(a.sts_client.__class__), "") def test_role_extraction(self): saml_xml = self.read_local_file('valid-response.xml') a = amazon.Amazon(self.valid_config, saml_xml) self.assertIsInstance(a.roles, dict) list_of_testing_roles = [ "arn:aws:iam::123456789012:role/admin", "arn:aws:iam::123456789012:role/read-only", "arn:aws:iam::123456789012:role/test"] self.assertEqual(sorted(list(a.roles.keys())), sorted(list_of_testing_roles)) def test_role_extraction_too_many_commas(self): # See https://github.com/cevoaustralia/aws-google-auth/issues/12 saml_xml = self.read_local_file('too-many-commas.xml') a = amazon.Amazon(self.valid_config, saml_xml) self.assertIsInstance(a.roles, dict) list_of_testing_roles = [ "arn:aws:iam::123456789012:role/admin", "arn:aws:iam::123456789012:role/read-only", "arn:aws:iam::123456789012:role/test"] self.assertEqual(sorted(list(a.roles.keys())), sorted(list_of_testing_roles)) def test_invalid_saml_too_soon(self): saml_xml = self.read_local_file('saml-response-too-soon.xml') self.assertFalse(amazon.Amazon.is_valid_saml_assertion(saml_xml)) def test_invalid_saml_too_late(self): saml_xml = self.read_local_file('saml-response-too-late.xml') self.assertFalse(amazon.Amazon.is_valid_saml_assertion(saml_xml)) def test_invalid_saml_expired_before_valid(self): saml_xml = self.read_local_file('saml-response-expired-before-valid.xml') self.assertFalse(amazon.Amazon.is_valid_saml_assertion(saml_xml)) def test_invalid_saml_bad_input(self): self.assertFalse(amazon.Amazon.is_valid_saml_assertion(None)) self.assertFalse(amazon.Amazon.is_valid_saml_assertion("Malformed Base64")) self.assertFalse(amazon.Amazon.is_valid_saml_assertion(123456)) self.assertFalse(amazon.Amazon.is_valid_saml_assertion('')) self.assertFalse(amazon.Amazon.is_valid_saml_assertion("QmFkIFhNTA==")) # Bad XML def test_valid_saml(self): saml_xml = self.read_local_file('saml-response-no-expire.xml') self.assertTrue(amazon.Amazon.is_valid_saml_assertion(saml_xml)) @mock.patch.dict(os.environ, {'AWS_PROFILE': 'xxx-xxxx', 'DEFAULT_AWS_PROFILE': 'blart'}) def test_sts_client_with_invalid_profile(self): a = amazon.Amazon(self.valid_config, "dummy-encoded-saml") self.assertIsNotNone(a.sts_client) self.assertEqual('xxx-xxxx', os.environ['AWS_PROFILE']) self.assertEqual('blart', os.environ['DEFAULT_AWS_PROFILE']) ================================================ FILE: aws_google_auth/tests/test_args_parser.py ================================================ #!/usr/bin/env python import unittest from aws_google_auth import parse_args class TestPythonFailOnVersion(unittest.TestCase): def test_no_arguments(self): """ This test case exists to validate the default settings of the args parser. Changes that break these checks should be considered for backwards compatibility review. :return: """ parser = parse_args([]) self.assertTrue(parser.saml_cache) self.assertEqual(parser.saml_assertion, None) self.assertFalse(parser.ask_role) self.assertFalse(parser.print_creds) self.assertFalse(parser.keyring) self.assertFalse(parser.resolve_aliases) self.assertFalse(parser.disable_u2f, None) self.assertEqual(parser.duration, None) self.assertEqual(parser.auto_duration, False) self.assertEqual(parser.idp_id, None) self.assertEqual(parser.sp_id, None) self.assertEqual(parser.profile, None) self.assertEqual(parser.region, None) self.assertEqual(parser.role_arn, None) self.assertEqual(parser.username, None) self.assertEqual(parser.quiet, False) self.assertEqual(parser.bg_response, None) self.assertEqual(parser.account, None) self.assertFalse(parser.save_failure_html) self.assertFalse(parser.save_saml_flow) # Assert the size of the parameter so that new parameters trigger a review of this function # and the appropriate defaults are added here to track backwards compatibility in the future. self.assertEqual(len(vars(parser)), 21) def test_username(self): parser = parse_args(['-u', 'username@gmail.com']) self.assertTrue(parser.saml_cache) self.assertFalse(parser.ask_role) self.assertFalse(parser.keyring) self.assertFalse(parser.resolve_aliases) self.assertEqual(parser.duration, None) self.assertEqual(parser.auto_duration, False) self.assertEqual(parser.idp_id, None) self.assertEqual(parser.profile, None) self.assertEqual(parser.region, None) self.assertEqual(parser.role_arn, None) self.assertEqual(parser.username, 'username@gmail.com') self.assertEqual(parser.account, None) def test_nocache(self): parser = parse_args(['--no-cache']) self.assertFalse(parser.saml_cache) self.assertFalse(parser.ask_role) self.assertFalse(parser.keyring) self.assertFalse(parser.resolve_aliases) self.assertEqual(parser.duration, None) self.assertEqual(parser.auto_duration, False) self.assertEqual(parser.idp_id, None) self.assertEqual(parser.profile, None) self.assertEqual(parser.region, None) self.assertEqual(parser.role_arn, None) self.assertEqual(parser.username, None) self.assertEqual(parser.account, None) def test_resolvealiases(self): parser = parse_args(['--resolve-aliases']) self.assertTrue(parser.saml_cache) self.assertFalse(parser.ask_role) self.assertFalse(parser.keyring) self.assertTrue(parser.resolve_aliases) self.assertEqual(parser.duration, None) self.assertEqual(parser.auto_duration, False) self.assertEqual(parser.idp_id, None) self.assertEqual(parser.profile, None) self.assertEqual(parser.region, None) self.assertEqual(parser.role_arn, None) self.assertEqual(parser.username, None) self.assertEqual(parser.account, None) def test_ask_and_supply_role(self): with self.assertRaises(SystemExit): parse_args(['-a', '-r', 'da-role']) def test_invalid_duration(self): """ Should fail parsing a non-int value for `-d`. :return: """ with self.assertRaises(SystemExit): parse_args(['-d', 'abce']) ================================================ FILE: aws_google_auth/tests/test_backwards_compatibility.py ================================================ #!/usr/bin/env python import unittest from random import randint import configparser from aws_google_auth import configuration class TestConfigurationPersistence(unittest.TestCase): def setUp(self): self.c = configuration.Configuration() # Pick a profile name that is clear it's for testing. We'll delete it # after, but in case something goes wrong we don't want to use # something that could clobber user input. self.c.profile = "aws_google_auth_test_{}".format(randint(100, 999)) # Pick a string to do password leakage tests. self.c.password = "aws_google_auth_test_password_{}".format(randint(100, 999)) self.c.region = "us-east-1" self.c.ask_role = False self.c.duration = 1234 self.c.idp_id = "sample_idp_id" self.c.role_arn = "arn:aws:iam::sample_arn" self.c.sp_id = "sample_sp_id" self.c.u2f_disabled = False self.c.username = "sample_username" self.c.account = "123456789012" self.c.raise_if_invalid() self.c.write(None) def tearDown(self): section_name = configuration.Configuration.config_profile(self.c.profile) self.config_parser.remove_section(section_name) with open(self.c.config_file, 'w') as config_file: self.config_parser.write(config_file) def test_configuration_backwards_compatibility(self): # Configuration self.config_parser = configparser.RawConfigParser() self.config_parser.read(self.c.config_file) profile_string = configuration.Configuration.config_profile(self.c.profile) self.assertTrue(self.config_parser.has_section(profile_string)) self.assertEqual(self.config_parser[profile_string].get('google_config.google_idp_id'), self.c.idp_id) self.assertEqual(self.config_parser[profile_string].get('google_config.role_arn'), self.c.role_arn) self.assertEqual(self.config_parser[profile_string].get('google_config.google_sp_id'), self.c.sp_id) self.assertEqual(self.config_parser[profile_string].get('google_config.google_username'), self.c.username) self.assertEqual(self.config_parser[profile_string].get('region'), self.c.region) self.assertEqual(self.config_parser[profile_string].getboolean('google_config.ask_role'), self.c.ask_role) self.assertEqual(self.config_parser[profile_string].getboolean('google_config.u2f_disabled'), self.c.u2f_disabled) self.assertEqual(self.config_parser[profile_string].getint('google_config.duration'), self.c.duration) ================================================ FILE: aws_google_auth/tests/test_config_parser.py ================================================ import os import unittest import mock from nose.tools import nottest from aws_google_auth import resolve_config, parse_args class TestProfileProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual("sts", config.profile) def test_cli_param_supplied(self): args = parse_args(['-p', 'profile']) config = resolve_config(args) self.assertEqual('profile', config.profile) @mock.patch.dict(os.environ, {'AWS_PROFILE': 'mytemp'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual('mytemp', config.profile) args = parse_args(['-p', 'profile']) config = resolve_config(args) self.assertEqual('profile', config.profile) class TestUsernameProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(None, config.username) def test_cli_param_supplied(self): args = parse_args(['-u', 'user@gmail.com']) config = resolve_config(args) self.assertEqual('user@gmail.com', config.username) @mock.patch.dict(os.environ, {'GOOGLE_USERNAME': 'override@gmail.com'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual('override@gmail.com', config.username) args = parse_args(['-u', 'user@gmail.com']) config = resolve_config(args) self.assertEqual('user@gmail.com', config.username) class TestDurationProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(43200, config.duration) def test_cli_param_supplied(self): args = parse_args(['-d', "500"]) config = resolve_config(args) self.assertEqual(500, config.duration) def test_invalid_cli_param_supplied(self): with self.assertRaises(SystemExit): args = parse_args(['-d', "blart"]) resolve_config(args) @mock.patch.dict(os.environ, {'DURATION': '3000'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(3000, config.duration) args = parse_args(['-d', "500"]) config = resolve_config(args) self.assertEqual(500, config.duration) class TestIDPProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(None, config.idp_id) def test_cli_param_supplied(self): args = parse_args(['-I', "kjl2342"]) config = resolve_config(args) self.assertEqual("kjl2342", config.idp_id) @mock.patch.dict(os.environ, {'GOOGLE_IDP_ID': 'adsfasf233423'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual("adsfasf233423", config.idp_id) args = parse_args(['-I', "kjl2342"]) config = resolve_config(args) self.assertEqual("kjl2342", config.idp_id) class TestSPProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(None, config.sp_id) def test_cli_param_supplied(self): args = parse_args(['-S', "kjl2342"]) config = resolve_config(args) self.assertEqual("kjl2342", config.sp_id) @mock.patch.dict(os.environ, {'GOOGLE_SP_ID': 'adsfasf233423'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual("adsfasf233423", config.sp_id) args = parse_args(['-S', "kjl2342"]) config = resolve_config(args) self.assertEqual("kjl2342", config.sp_id) class TestRegionProcessing(unittest.TestCase): @nottest def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(None, config.region) def test_cli_param_supplied(self): args = parse_args(['--region', "ap-southeast-4"]) config = resolve_config(args) self.assertEqual("ap-southeast-4", config.region) @mock.patch.dict(os.environ, {'AWS_DEFAULT_REGION': 'ap-southeast-9'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual("ap-southeast-9", config.region) args = parse_args(['--region', "ap-southeast-4"]) config = resolve_config(args) self.assertEqual("ap-southeast-4", config.region) class TestRoleProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(None, config.role_arn) def test_cli_param_supplied(self): args = parse_args(['-r', "role1234"]) config = resolve_config(args) self.assertEqual("role1234", config.role_arn) @mock.patch.dict(os.environ, {'AWS_ROLE_ARN': '4567-role'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual("4567-role", config.role_arn) class TestAskRoleProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertFalse(config.ask_role) def test_cli_param_supplied(self): args = parse_args(['-a']) config = resolve_config(args) self.assertTrue(config.ask_role) @nottest @mock.patch.dict(os.environ, {'AWS_ASK_ROLE': 'true'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertTrue(config.ask_role) class TestU2FDisabledProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertFalse(config.u2f_disabled) def test_cli_param_supplied(self): args = parse_args(['-D']) config = resolve_config(args) self.assertTrue(config.u2f_disabled) @nottest @mock.patch.dict(os.environ, {'U2F_DISABLED': 'true'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertTrue(config.u2f_disabled) class TestResolveAliasesProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertFalse(config.resolve_aliases) def test_cli_param_supplied(self): args = parse_args(['--resolve-aliases']) config = resolve_config(args) self.assertTrue(config.resolve_aliases) @nottest @mock.patch.dict(os.environ, {'RESOLVE_AWS_ALIASES': 'true'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertTrue(config.resolve_aliases) class TestBgResponseProcessing(unittest.TestCase): def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertFalse(config.resolve_aliases) def test_cli_param_supplied(self): args = parse_args(['--bg-response=foo']) config = resolve_config(args) self.assertEqual(config.bg_response, 'foo') @nottest @mock.patch.dict(os.environ, {'GOOGLE_BG_RESPONSE': 'foo'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(config.bg_response, 'foo') class TestAccountProcessing(unittest.TestCase): @nottest def test_default(self): args = parse_args([]) config = resolve_config(args) self.assertEqual(None, config.account) def test_cli_param_supplied(self): args = parse_args(['--account', "123456789012"]) config = resolve_config(args) self.assertEqual("123456789012", config.account) @mock.patch.dict(os.environ, {'AWS_ACCOUNT': '123456789012'}) def test_with_environment(self): args = parse_args([]) config = resolve_config(args) self.assertEqual("123456789012", config.account) args = parse_args(['--region', "123456789012"]) config = resolve_config(args) self.assertEqual("123456789012", config.account) ================================================ FILE: aws_google_auth/tests/test_configuration.py ================================================ #!/usr/bin/env python import unittest from aws_google_auth import configuration class TestConfigurationMethods(unittest.TestCase): def test_config_profile(self): self.assertEqual(configuration.Configuration.config_profile('default'), 'default') self.assertEqual(configuration.Configuration.config_profile('DEFAULT'), 'DEFAULT') self.assertEqual(configuration.Configuration.config_profile('testing'), 'profile testing') self.assertEqual(configuration.Configuration.config_profile(None), 'profile None') self.assertEqual(configuration.Configuration.config_profile(123456), 'profile 123456') def test_duration_invalid_values(self): # Duration must be an integer c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.sp_id = "sample_sp_id" c.username = "sample_username" c.duration = "bad_type" c.region = "sample_region" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected duration to be an integer.", str(e.exception)) # Duration can not be negative c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.duration = -1 with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected duration to be greater than or equal to 900.", str(e.exception)) # Duration can not be greater than MAX_DURATION valid = configuration.Configuration() valid.idp_id = "sample_idp_id" c.password = "hunter2" valid.sp_id = "sample_sp_id" valid.username = "sample_username" valid.duration = 900 c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.duration = (valid.max_duration + 1) with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected duration to be less than or equal to max_duration", str(e.exception)) def test_duration_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.duration = 900 self.assertEqual(c.duration, 900) c.raise_if_invalid() c.duration = c.max_duration self.assertEqual(c.duration, c.max_duration) c.raise_if_invalid() c.duration = (c.max_duration - 1) self.assertEqual(c.duration, c.max_duration - 1) c.raise_if_invalid() def test_duration_defaults_to_max_duration(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" self.assertEqual(c.duration, c.max_duration) c.raise_if_invalid() def test_ask_role_invalid_values(self): # ask_role must be a boolean c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.ask_role = "bad_value" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected ask_role to be a boolean.", str(e.exception)) def test_ask_role_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.ask_role = True self.assertTrue(c.ask_role) c.raise_if_invalid() c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.sp_id = "sample_sp_id" c.username = "sample_username" c.ask_role = False self.assertFalse(c.ask_role) c.raise_if_invalid() def test_ask_role_optional(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" self.assertFalse(c.ask_role) c.raise_if_invalid() def test_idp_id_invalid_values(self): # idp_id must not be None c = configuration.Configuration() c.region = "sample_region" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected idp_id to be set to non-None value.", str(e.exception)) def test_idp_id_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" self.assertEqual(c.idp_id, "sample_idp_id") c.raise_if_invalid() c.idp_id = 123456 self.assertEqual(c.idp_id, 123456) c.raise_if_invalid() def test_sp_id_invalid_values(self): # sp_id must not be None c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.username = "sample_username" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected sp_id to be set to non-None value.", str(e.exception)) def test_username_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.password = "hunter2" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" self.assertEqual(c.username, "sample_username") c.raise_if_invalid() c.username = "123456" self.assertEqual(c.username, "123456") c.raise_if_invalid() def test_username_invalid_values(self): # username must be set c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.sp_id = "sample_sp_id" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected username to be a string.", str(e.exception)) # username must be be string c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = 123456 with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected username to be a string.", str(e.exception)) def test_password_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.password = "hunter2" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" self.assertEqual(c.password, "hunter2") c.raise_if_invalid() c.password = "123456" self.assertEqual(c.password, "123456") c.raise_if_invalid() def test_password_invalid_values(self): # password must be set c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.username = "sample_username" c.sp_id = "sample_sp_id" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected password to be a string.", str(e.exception)) # password must be be string c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = 123456 c.username = "sample_username" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected password to be a string.", str(e.exception)) def test_sp_id_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.password = "hunter2" self.assertEqual(c.sp_id, "sample_sp_id") c.raise_if_invalid() c.sp_id = 123456 self.assertEqual(c.sp_id, 123456) c.raise_if_invalid() def test_profile_defaults_to_sts(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.sp_id = "sample_sp_id" c.username = "sample_username" self.assertEqual(c.profile, "sts") c.raise_if_invalid() def test_profile_invalid_values(self): # profile must be a string c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.profile = 123456 with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected profile to be a string.", str(e.exception)) def test_profile_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.sp_id = "sample_sp_id" c.username = "sample_username" c.profile = "default" self.assertEqual(c.profile, "default") c.raise_if_invalid() c.profile = "sts" self.assertEqual(c.profile, "sts") c.raise_if_invalid() def test_profile_defaults(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.password = "hunter2" c.sp_id = "sample_sp_id" c.username = "sample_username" self.assertEqual(c.profile, 'sts') c.raise_if_invalid() def test_region_invalid_values(self): # region must be a string c = configuration.Configuration() c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.region = 1234 with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected region to be a string.", str(e.exception)) def test_region_valid_values(self): c = configuration.Configuration() c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.region = "us-east-1" self.assertEqual(c.region, "us-east-1") c.raise_if_invalid() c.region = "us-west-2" self.assertEqual(c.region, "us-west-2") c.raise_if_invalid() def test_region_defaults_to_none(self): c = configuration.Configuration() c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.password = "hunter2" self.assertEqual(c.region, None) with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected region to be a string.", str(e.exception)) def test_role_arn_invalid_values(self): # role_arn must be a string c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.role_arn = 1234 with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected role_arn to be None or a string.", str(e.exception)) # role_arn be a arn-looking string c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" c.role_arn = "bad_string" with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected role_arn to contain 'arn:aws:iam::'", str(e.exception)) def test_role_arn_is_optional(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.password = "hunter2" c.username = "sample_username" self.assertIsNone(c.role_arn) c.raise_if_invalid() def test_role_arn_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.password = "hunter2" c.role_arn = "arn:aws:iam::some_arn_1" self.assertEqual(c.role_arn, "arn:aws:iam::some_arn_1") c.raise_if_invalid() c.role_arn = "arn:aws:iam::some_other_arn_2" self.assertEqual(c.role_arn, "arn:aws:iam::some_other_arn_2") c.raise_if_invalid() def test_u2f_disabled_invalid_values(self): # u2f_disabled must be a boolean c = configuration.Configuration() c.region = "sample_region" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.password = "hunter2" c.u2f_disabled = 1234 with self.assertRaises(AssertionError) as e: c.raise_if_invalid() self.assertIn("Expected u2f_disabled to be a boolean.", str(e.exception)) def test_u2f_disabled_valid_values(self): c = configuration.Configuration() c.region = "sample_region" c.password = "hunter2" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.u2f_disabled = True self.assertTrue(c.u2f_disabled) c.raise_if_invalid() c = configuration.Configuration() c.region = "sample_region" c.password = "hunter2" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.u2f_disabled = False self.assertFalse(c.u2f_disabled) c.raise_if_invalid() def test_u2f_disabled_is_optional(self): c = configuration.Configuration() c.region = "sample_region" c.password = "hunter2" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" self.assertFalse(c.u2f_disabled) c.raise_if_invalid() def test_unicode_password(self): c = configuration.Configuration() c.region = "sample_region" c.password = u"hunter2" c.idp_id = "sample_idp_id" c.sp_id = "sample_sp_id" c.username = "sample_username" c.raise_if_invalid() ================================================ FILE: aws_google_auth/tests/test_configuration_persistence.py ================================================ #!/usr/bin/env python import unittest from random import randint import configparser from aws_google_auth import configuration class TestConfigurationPersistence(unittest.TestCase): def setUp(self): self.c = configuration.Configuration() # Pick a profile name that is clear it's for testing. We'll delete it # after, but in case something goes wrong we don't want to use # something that could clobber user input. self.c.profile = "aws_google_auth_test_{}".format(randint(100, 999)) # Pick a string to do password leakage tests. self.c.password = "aws_google_auth_test_password_{}".format(randint(100, 999)) self.c.region = "us-east-1" self.c.ask_role = False self.c.keyring = False self.c.duration = 1234 self.c.idp_id = "sample_idp_id" self.c.role_arn = "arn:aws:iam::sample_arn" self.c.sp_id = "sample_sp_id" self.c.u2f_disabled = False self.c.username = "sample_username" self.c.bg_response = "foo" self.c.raise_if_invalid() self.c.write(None) self.c.account = "123456789012" self.config_parser = configparser.RawConfigParser() self.config_parser.read(self.c.config_file) def tearDown(self): section_name = configuration.Configuration.config_profile(self.c.profile) self.config_parser.remove_section(section_name) with open(self.c.config_file, 'w') as config_file: self.config_parser.write(config_file) def test_creating_new_profile(self): profile_string = configuration.Configuration.config_profile(self.c.profile) self.assertTrue(self.config_parser.has_section(profile_string)) self.assertEqual(self.config_parser[profile_string].get('google_config.google_idp_id'), self.c.idp_id) self.assertEqual(self.config_parser[profile_string].get('google_config.role_arn'), self.c.role_arn) self.assertEqual(self.config_parser[profile_string].get('google_config.google_sp_id'), self.c.sp_id) self.assertEqual(self.config_parser[profile_string].get('google_config.google_username'), self.c.username) self.assertEqual(self.config_parser[profile_string].get('region'), self.c.region) self.assertEqual(self.config_parser[profile_string].getboolean('google_config.ask_role'), self.c.ask_role) self.assertEqual(self.config_parser[profile_string].getboolean('google_config.keyring'), self.c.keyring) self.assertEqual(self.config_parser[profile_string].getboolean('google_config.u2f_disabled'), self.c.u2f_disabled) self.assertEqual(self.config_parser[profile_string].getint('google_config.duration'), self.c.duration) self.assertEqual(self.config_parser[profile_string].get('google_config.bg_response'), self.c.bg_response) def test_password_not_written(self): profile_string = configuration.Configuration.config_profile(self.c.profile) self.assertIsNone(self.config_parser[profile_string].get('google_config.password', None)) self.assertIsNone(self.config_parser[profile_string].get('password', None)) # Check for password leakage (It didn't get written in an odd way) with open(self.c.config_file, 'r') as config_file: for line in config_file: self.assertFalse(self.c.password in line) def test_can_read_all_values(self): test_configuration = configuration.Configuration() test_configuration.read(self.c.profile) # Reading won't get password, so we need to set for the configuration # to be considered valid test_configuration.password = "test_password" test_configuration.raise_if_invalid() self.assertEqual(test_configuration.profile, self.c.profile) self.assertEqual(test_configuration.idp_id, self.c.idp_id) self.assertEqual(test_configuration.role_arn, self.c.role_arn) self.assertEqual(test_configuration.sp_id, self.c.sp_id) self.assertEqual(test_configuration.username, self.c.username) self.assertEqual(test_configuration.region, self.c.region) self.assertEqual(test_configuration.ask_role, self.c.ask_role) self.assertEqual(test_configuration.u2f_disabled, self.c.u2f_disabled) self.assertEqual(test_configuration.duration, self.c.duration) self.assertEqual(test_configuration.keyring, self.c.keyring) self.assertEqual(test_configuration.bg_response, self.c.bg_response) ================================================ FILE: aws_google_auth/tests/test_google.py ================================================ # -*- coding: utf-8 -*- import unittest from io import open from os import path import json import base64 from bs4 import BeautifulSoup from mock import Mock from aws_google_auth import google class TestGoogle(unittest.TestCase): def read_local_file(self, filename): here = path.abspath(path.dirname(__file__)) with open(path.join(here, filename), encoding='utf-8') as fp: return fp.read().encode('utf-8') def test_extra_step(self): response = self.read_local_file('google_error.html') response = BeautifulSoup(response, 'html.parser') with self.assertRaises(ValueError): google.Google.check_extra_step(response) def test_find_keyhandles(self): challenges_txt = "RFVNTVlDSEFMTEVOR0U=" keyHandleJSText = """{"1010":[2,true,0,false] ,"5010":[null,null,null,"https://accounts.google.com/signin/challenge/sk/5",null,["google.com","RFVNTVlDSEFMTEVOR0U\\u003d",[[2,"S0VZSEFORExFMQ\\u003d\\u003d",[1] ] ,[2,"S0VZSEFORExFMg\\u003d\\u003d",[1,2] ] ] ,"{\\"appid\\":\\"https://www.gstatic.com/securitykey/origins.json\\"}"] ] } """ keyHandleJsonPayload = json.loads(keyHandleJSText) keyHandles = google.Google.find_key_handles(keyHandleJsonPayload, base64.urlsafe_b64encode(base64.b64decode(challenges_txt))) self.assertEqual( [ b"S0VZSEFORExFMQ==", b"S0VZSEFORExFMg==", ], keyHandles, ) def test_parse_saml_without_login(self): mock_config = Mock() undertest = google.Google(config=mock_config, save_failure=False) with self.assertRaises(RuntimeError) as ex: undertest.parse_saml() self.assertEqual("You must use do_login() before calling parse_saml()", str(ex.exception)) def test_parse_saml_without_save(self): mock_config = Mock() mock_config.profile = False mock_config.saml_cache = False mock_config.keyring = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.return_value = None mock_config.print_creds = True undertest = google.Google(config=mock_config, save_failure=False) undertest.session_state = Mock() undertest.session_state.text = "" with self.assertRaises(google.ExpectedGoogleException) as ex: undertest.parse_saml() self.assertEqual("Something went wrong - Could not find SAML response, check your credentials " "or use --save-failure-html to debug.", str(ex.exception)) def test_parse_saml_with_save(self): mock_config = Mock() mock_config.profile = False mock_config.saml_cache = False mock_config.keyring = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.return_value = None mock_config.print_creds = True undertest = google.Google(config=mock_config, save_failure=True) undertest.session_state = Mock() undertest.session_state.text = "" with self.assertRaises(google.ExpectedGoogleException) as ex: undertest.parse_saml() self.assertEqual("Something went wrong - Could not find SAML response, check your credentials " "or use --save-failure-html to debug.", str(ex.exception)) ================================================ FILE: aws_google_auth/tests/test_init.py ================================================ import unittest from argparse import Namespace from mock import call, patch, Mock, MagicMock import aws_google_auth class TestInit(unittest.TestCase): def setUp(self): pass @patch('aws_google_auth.cli', spec=True) def test_main_method_has_no_parameters(self, mock_cli): """ This is the entrypoint for the cli tool, and should require no parameters :param mock_cli: :return: """ # Function under test aws_google_auth.main() self.assertTrue(mock_cli.called) @patch('aws_google_auth.exit_if_unsupported_python', spec=True) @patch('aws_google_auth.resolve_config', spec=True) @patch('aws_google_auth.process_auth', spec=True) def test_main_method_chaining(self, process_auth, resolve_config, exit_if_unsupported_python): # Create a mock config to be returned from the resolve_config function mock_config = Mock() # Inject the mock as the return value from the function aws_google_auth.resolve_config.return_value = mock_config # Function under test aws_google_auth.cli([]) self.assertTrue(exit_if_unsupported_python.called) self.assertTrue(resolve_config.called) self.assertTrue(process_auth.called) self.assertEqual([call()], exit_if_unsupported_python.mock_calls) self.assertEqual([call(Namespace(ask_role=False, keyring=False, disable_u2f=False, duration=None, auto_duration=False, idp_id=None, profile=None, region=None, resolve_aliases=False, role_arn=None, save_failure_html=False, save_saml_flow=False, saml_cache=True, saml_assertion=None, sp_id=None, log_level='warn', print_creds=False, username=None, quiet=False, bg_response=None, account=None)) ], resolve_config.mock_calls) self.assertEqual([call(Namespace(ask_role=False, keyring=False, disable_u2f=False, duration=None, auto_duration=False, idp_id=None, profile=None, region=None, resolve_aliases=False, role_arn=None, save_failure_html=False, save_saml_flow=False, saml_cache=True, saml_assertion=None, sp_id=None, log_level='warn', print_creds=False, username=None, quiet=False, bg_response=None, account=None), mock_config) ], process_auth.mock_calls) @patch('aws_google_auth.util', spec=True) @patch('aws_google_auth.amazon', spec=True) @patch('aws_google_auth.google', spec=True) def test_process_auth_standard(self, mock_google, mock_amazon, mock_util): mock_config = Mock() mock_config.profile = False mock_config.saml_cache = False mock_config.keyring = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.return_value = None mock_config.account = None mock_config.region = None mock_amazon_client = Mock() mock_google_client = Mock() mock_amazon_client.roles = { 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' } mock_util_obj = MagicMock() mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider")) mock_util_obj.get_input = MagicMock(side_effect=["region_input", "input", "input2", "input3"]) mock_util_obj.get_password = MagicMock(return_value="pass") mock_util.Util = mock_util_obj mock_amazon_client.resolve_aws_aliases = MagicMock(return_value=[]) mock_amazon.Amazon = MagicMock(return_value=mock_amazon_client) mock_google.Google = MagicMock(return_value=mock_google_client) args = aws_google_auth.parse_args([]) # Method Under Test aws_google_auth.process_auth(args, mock_config) # Assert values collected self.assertEqual(mock_config.region, "region_input") self.assertEqual(mock_config.username, "input") self.assertEqual(mock_config.idp_id, "input2") self.assertEqual(mock_config.sp_id, "input3") self.assertEqual(mock_config.password, "pass") self.assertEqual(mock_config.provider, "da_provider") self.assertEqual(mock_config.role_arn, "da_role") # Assert calls occur self.assertEqual([call.Util.get_input('AWS Region: '), call.Util.get_input('Google username: '), call.Util.get_input('Google IDP ID: '), call.Util.get_input('Google SP ID: '), call.Util.get_password('Google Password: '), call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])], mock_util.mock_calls) self.assertEqual([call.do_login(), call.parse_saml()], mock_google_client.mock_calls) self.assertEqual([call.raise_if_invalid()], mock_config.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' })], mock_amazon_client.resolve_aws_aliases.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, []) ], mock_util_obj.pick_a_role.mock_calls) @patch('aws_google_auth.util', spec=True) @patch('aws_google_auth.amazon', spec=True) @patch('aws_google_auth.google', spec=True) def test_process_auth_print_creds(self, mock_google, mock_amazon, mock_util): mock_config = Mock() mock_config.profile = False mock_config.saml_cache = False mock_config.keyring = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.return_value = None mock_config.print_creds = True mock_config.account = None mock_amazon_client = Mock() mock_google_client = Mock() mock_amazon_client.roles = { 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' } mock_util_obj = MagicMock() mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider")) mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"]) mock_util_obj.get_password = MagicMock(return_value="pass") mock_util.Util = mock_util_obj mock_amazon_client.resolve_aws_aliases = MagicMock(return_value=[]) mock_amazon_client.print_export_line = Mock() mock_amazon.Amazon = MagicMock(return_value=mock_amazon_client) mock_google.Google = MagicMock(return_value=mock_google_client) args = aws_google_auth.parse_args([]) # Method Under Test aws_google_auth.process_auth(args, mock_config) # Assert values collected self.assertEqual(mock_config.username, "input") self.assertEqual(mock_config.idp_id, "input2") self.assertEqual(mock_config.sp_id, "input3") self.assertEqual(mock_config.password, "pass") self.assertEqual(mock_config.provider, "da_provider") self.assertEqual(mock_config.role_arn, "da_role") # Assert calls occur self.assertEqual([call.Util.get_input('Google username: '), call.Util.get_input('Google IDP ID: '), call.Util.get_input('Google SP ID: '), call.Util.get_password('Google Password: '), call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])], mock_util.mock_calls) self.assertEqual([call.do_login(), call.parse_saml()], mock_google_client.mock_calls) self.assertEqual([call.raise_if_invalid()], mock_config.mock_calls) self.assertEqual( [call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' })], mock_amazon_client.resolve_aws_aliases.mock_calls) self.assertEqual( [call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, []) ], mock_util_obj.pick_a_role.mock_calls) self.assertEqual([call()], mock_amazon_client.print_export_line.mock_calls) @patch('aws_google_auth.util', spec=True) @patch('aws_google_auth.amazon', spec=True) @patch('aws_google_auth.google', spec=True) def test_process_auth_specified_role(self, mock_google, mock_amazon, mock_util): mock_config = Mock() mock_config.saml_cache = False mock_config.keyring = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.return_value = None mock_config.role_arn = 'arn:aws:iam::123456789012:role/admin' mock_config.ask_role = False mock_amazon_client = Mock() mock_google_client = Mock() mock_amazon_client.roles = { 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' } mock_util_obj = MagicMock() mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider")) mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"]) mock_util_obj.get_password = MagicMock(return_value="pass") mock_util.Util = mock_util_obj mock_amazon_client.resolve_aws_aliases = MagicMock(return_value=[]) mock_amazon.Amazon = MagicMock(return_value=mock_amazon_client) mock_google.Google = MagicMock(return_value=mock_google_client) args = aws_google_auth.parse_args([]) # Method Under Test aws_google_auth.process_auth(args, mock_config) # Assert values collected self.assertEqual(mock_config.username, "input") self.assertEqual(mock_config.idp_id, "input2") self.assertEqual(mock_config.sp_id, "input3") self.assertEqual(mock_config.password, "pass") self.assertEqual(mock_config.provider, "arn:aws:iam::123456789012:saml-provider/GoogleApps") self.assertEqual(mock_config.role_arn, "arn:aws:iam::123456789012:role/admin") # Assert calls occur self.assertEqual([call.Util.get_input('Google username: '), call.Util.get_input('Google IDP ID: '), call.Util.get_input('Google SP ID: '), call.Util.get_password('Google Password: ')], mock_util.mock_calls) self.assertEqual([call.do_login(), call.parse_saml()], mock_google_client.mock_calls) self.assertEqual([call.raise_if_invalid(), call.write(mock_amazon_client)], mock_config.mock_calls) self.assertEqual([], mock_amazon_client.resolve_aws_aliases.mock_calls) self.assertEqual([], mock_util_obj.pick_a_role.mock_calls) @patch('aws_google_auth.util', spec=True) @patch('aws_google_auth.amazon', spec=True) @patch('aws_google_auth.google', spec=True) def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazon, mock_util): mock_config = Mock() mock_config.saml_cache = False mock_config.resolve_aliases = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.return_value = None mock_config.keyring = False mock_config.account = None mock_amazon_client = Mock() mock_google_client = Mock() mock_amazon_client.roles = { 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' } mock_util_obj = MagicMock() mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider")) mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"]) mock_util_obj.get_password = MagicMock(return_value="pass") mock_util.Util = mock_util_obj mock_amazon_client.resolve_aws_aliases = MagicMock(return_value=[]) mock_amazon.Amazon = MagicMock(return_value=mock_amazon_client) mock_google.Google = MagicMock(return_value=mock_google_client) args = aws_google_auth.parse_args([]) # Method Under Test aws_google_auth.process_auth(args, mock_config) # Assert values collected self.assertEqual(mock_config.username, "input") self.assertEqual(mock_config.idp_id, "input2") self.assertEqual(mock_config.sp_id, "input3") self.assertEqual(mock_config.password, "pass") self.assertEqual(mock_config.provider, "da_provider") self.assertEqual(mock_config.role_arn, "da_role") self.assertEqual(mock_config.account, None) # Assert calls occur self.assertEqual([call.Util.get_input('Google username: '), call.Util.get_input('Google IDP ID: '), call.Util.get_input('Google SP ID: '), call.Util.get_password('Google Password: '), call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'})], mock_util.mock_calls) self.assertEqual([call.do_login(), call.parse_saml()], mock_google_client.mock_calls) self.assertEqual([call.raise_if_invalid(), call.write(mock_amazon_client)], mock_config.mock_calls) self.assertEqual([], mock_amazon_client.resolve_aws_aliases.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}) ], mock_util_obj.pick_a_role.mock_calls) @patch('aws_google_auth.util', spec=True) @patch('aws_google_auth.amazon', spec=True) @patch('aws_google_auth.google', spec=True) def test_process_auth_with_profile(self, mock_google, mock_amazon, mock_util): mock_config = Mock() mock_config.saml_cache = False mock_config.keyring = False mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.profile = "blart" mock_config.return_value = None mock_config.role_arn = 'arn:aws:iam::123456789012:role/admin' mock_config.account = None mock_amazon_client = Mock() mock_google_client = Mock() mock_amazon_client.roles = { 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' } mock_util_obj = MagicMock() mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider")) mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"]) mock_util_obj.get_password = MagicMock(return_value="pass") mock_util.Util = mock_util_obj mock_amazon_client.resolve_aws_aliases = MagicMock(return_value=[]) mock_amazon.Amazon = MagicMock(return_value=mock_amazon_client) mock_google.Google = MagicMock(return_value=mock_google_client) args = aws_google_auth.parse_args([]) # Method Under Test aws_google_auth.process_auth(args, mock_config) # Assert values collected self.assertEqual(mock_config.username, "input") self.assertEqual(mock_config.idp_id, "input2") self.assertEqual(mock_config.sp_id, "input3") self.assertEqual(mock_config.password, "pass") self.assertEqual(mock_config.provider, "da_provider") self.assertEqual(mock_config.role_arn, "da_role") # Assert calls occur self.assertEqual([call.Util.get_input('Google username: '), call.Util.get_input('Google IDP ID: '), call.Util.get_input('Google SP ID: '), call.Util.get_password('Google Password: '), call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])], mock_util.mock_calls) self.assertEqual([call.do_login(), call.parse_saml()], mock_google_client.mock_calls) self.assertEqual([call.raise_if_invalid(), call.write(mock_amazon_client)], mock_config.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' })], mock_amazon_client.resolve_aws_aliases.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, []) ], mock_util_obj.pick_a_role.mock_calls) @patch('aws_google_auth.util', spec=True) @patch('aws_google_auth.amazon', spec=True) @patch('aws_google_auth.google', spec=True) def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, mock_util): mock_config = Mock() mock_config.saml_cache = True mock_config.username = None mock_config.idp_id = None mock_config.sp_id = None mock_config.password = None mock_config.return_value = None mock_config.role_arn = 'arn:aws:iam::123456789012:role/admin' mock_config.account = None mock_amazon_client = Mock() mock_google_client = Mock() mock_amazon_client.roles = { 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' } mock_util_obj = MagicMock() mock_util_obj.pick_a_role = MagicMock(return_value=("da_role", "da_provider")) mock_util_obj.get_input = MagicMock(side_effect=["input", "input2", "input3"]) mock_util_obj.get_password = MagicMock(return_value="pass") mock_util.Util = mock_util_obj mock_amazon_client.resolve_aws_aliases = MagicMock(return_value=[]) mock_amazon.Amazon = MagicMock(return_value=mock_amazon_client) mock_google.Google = MagicMock(return_value=mock_google_client) args = aws_google_auth.parse_args([]) # Method Under Test aws_google_auth.process_auth(args, mock_config) # Assert values collected self.assertEqual(mock_config.username, None) self.assertEqual(mock_config.idp_id, None) self.assertEqual(mock_config.sp_id, None) self.assertEqual(mock_config.password, None) self.assertEqual(mock_config.provider, "da_provider") self.assertEqual(mock_config.role_arn, "da_role") # Assert calls occur self.assertEqual([call.Util.pick_a_role({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, [])], mock_util.mock_calls) # Cache means no google calls self.assertEqual([], mock_google_client.mock_calls) self.assertEqual([call.write(mock_amazon_client)], mock_config.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps' })], mock_amazon_client.resolve_aws_aliases.mock_calls) self.assertEqual([call({'arn:aws:iam::123456789012:role/read-only': 'arn:aws:iam::123456789012:saml-provider/GoogleApps', 'arn:aws:iam::123456789012:role/admin': 'arn:aws:iam::123456789012:saml-provider/GoogleApps'}, []) ], mock_util_obj.pick_a_role.mock_calls) ================================================ FILE: aws_google_auth/tests/test_python_version.py ================================================ from aws_google_auth import exit_if_unsupported_python import unittest import sys import mock class TestPythonFailOnVersion(unittest.TestCase): def test_python26(self): with mock.patch.object(sys, 'version_info') as v_info: v_info.major = 2 v_info.minor = 6 with self.assertRaises(SystemExit) as cm: exit_if_unsupported_python() self.assertEqual(cm.exception.code, 1) def test_python27(self): with mock.patch.object(sys, 'version_info') as v_info: v_info.major = 2 v_info.minor = 7 try: exit_if_unsupported_python() except SystemExit: self.fail("exit_if_unsupported_python() raised SystemExit unexpectedly!") def test_python30(self): with mock.patch.object(sys, 'version_info') as v_info: v_info.major = 3 v_info.minor = 0 try: exit_if_unsupported_python() except SystemExit: self.fail("exit_if_unsupported_python() raised SystemExit unexpectedly!") ================================================ FILE: aws_google_auth/tests/test_util.py ================================================ #!/usr/bin/env python import sys import unittest from mock import patch, MagicMock from aws_google_auth import util class TestUtilMethods(unittest.TestCase): def test_coalesce_no_arguments(self): self.assertEqual(util.Util.coalesce(), None) def test_coalesce_one_argument(self): value = "non_none_value" self.assertEqual(util.Util.coalesce(value), value) self.assertEqual(util.Util.coalesce(None), None) def test_coalesce_two_arguments(self): value = "non_none_value" self.assertEqual(util.Util.coalesce(value, None), value) self.assertEqual(util.Util.coalesce(value, value), value) self.assertEqual(util.Util.coalesce(None, value), value) self.assertEqual(util.Util.coalesce(None, None), None) def test_coalesce_many_arguments(self): self.assertEqual(util.Util.coalesce(None, "test-01", None, "test-02", None, "test-03"), "test-01") self.assertEqual(util.Util.coalesce("test-01", None, "test-02", None, "test-03", None), "test-01") self.assertEqual(util.Util.coalesce(None, None, None, None, None, None, None, None, None, None, "test-01"), "test-01") def test_unicode_to_string_if_needed_python_3(self): if sys.version_info >= (3, 0): value_string = "Test String!" self.assertIn("str", str(value_string.__class__)) self.assertEqual(util.Util.unicode_to_string_if_needed(value_string), value_string) def test_unicode_to_string_if_needed_python_2(self): if sys.version_info < (3, 0): value_string = "Test String!" value_unicode = value_string.decode('utf-8') self.assertIn("str", str(value_string.__class__)) self.assertIn("unicode", str(value_unicode.__class__)) self.assertEqual(util.Util.unicode_to_string_if_needed(value_unicode), value_string) self.assertEqual(util.Util.unicode_to_string_if_needed(value_string), value_string) def test_unicode_to_string_if_needed(self): self.assertEqual(util.Util.unicode_to_string_if_needed(None), None) self.assertEqual(util.Util.unicode_to_string_if_needed(1234), 1234) self.assertEqual(util.Util.unicode_to_string_if_needed("nop"), "nop") @patch('getpass.getpass', spec=True) @patch('sys.stdin', spec=True) def test_get_password_when_tty(self, mock_stdin, mock_getpass): mock_stdin.isatty = MagicMock(return_value=True) mock_getpass.return_value = "pass" self.assertEqual(util.Util.get_password("Test: "), "pass") @patch('sys.stdin', spec=True) def test_get_password_when_not_tty(self, mock_stdin): mock_stdin.isatty = MagicMock(return_value=False) mock_stdin.readline = MagicMock(return_value="pass") self.assertEqual(util.Util.get_password("Test: "), "pass") ================================================ FILE: aws_google_auth/tests/too-many-commas.xml ================================================ https://accounts.google.com/o/saml2?idpid=abcd12345 https://accounts.google.com/o/saml2?idpid=abcd12345 GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y= dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+ VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0 SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA== ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. MIIDdDCCAlygAwIBAgIGAVXC/OcnMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzA3 MDEzMzE5WhcNMjEwNzA2MDEzMzE5WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAhkv0Sr7ALfc58YrnLXzVGfTRg1T9xUfuZqhdu80BgHTfaJDLX66icHHRRoso/hho EIYo1pUQTq0DtgmqkLg9rAup3rR+pImfcHBC55+vMDoEf5t88H/i0qDn3r63PxeULRoFIkCX9aVG uUPDe2CHAxB1UXUxyDf7ZAdIQJLPJdOQlsNRleBBoek4vuo2ZHv+A2tbAhE8/rIoQlDvXSpCZ9P7 m9TrFOb7tB4pHjJjESdmqcnEFc5zepAT8IuRAGZ1OkjJs74JUp+03do8scTMXzvVi4jefpyXhnoN C0da4OwPig7UmbDsrSCGbqz29UgxmGUmSnLchpkglw1eET5hTwIDAQABMA0GCSqGSIb3DQEBCwUA A4IBAQAA5WBtCPlaSIm1NIpKYd2x8qfeKc2YsxbAPukgUFaRDl1uxGw1HdzNzUp9X4JOF/futpw/ yhmw9o1GHBukIdj0mJRt8O9szRdkJmx4EfbY5bTVzkQ7QGv9FI1LBD6z6KgJEOxEGpDbh2Z8uyW8 HvxXgZgiyan53FauVJe+UuAkBy2ynJcVKK3+vUEISFXn1oh5SPOmi+2R4WKSgyTqOKpuowHHHg9u EbwwnXPMU4q3QLG1oDrp0ZvVuprvJaoWd5zIt/TYB3Hb5oEO7Imwx1n9K9QskYmFygR9rdJ6VS7L 6/h6rcL/dKjm4pU0Dgk9h9Hi8ps7Mn+nRRhsWQbiD59n first.last@example.com https://signin.aws.amazon.com/saml first.last@example.com arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps, arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps, arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps, 28800 urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ================================================ FILE: aws_google_auth/tests/valid-response.xml ================================================ https://accounts.google.com/o/saml2?idpid=abcd12345 https://accounts.google.com/o/saml2?idpid=abcd12345 GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y= dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+ VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0 SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA== ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc. MIIDdDCCAlygAwIBAgIGAVXC/OcnMA0GCSqGSIb3DQEBCwUAMHsxFDASBgNVBAoTC0dvb2dsZSBJ bmMuMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MQ8wDQYDVQQDEwZHb29nbGUxGDAWBgNVBAsTD0dv b2dsZSBGb3IgV29yazELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWEwHhcNMTYwNzA3 MDEzMzE5WhcNMjEwNzA2MDEzMzE5WjB7MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEWMBQGA1UEBxMN TW91bnRhaW4gVmlldzEPMA0GA1UEAxMGR29vZ2xlMRgwFgYDVQQLEw9Hb29nbGUgRm9yIFdvcmsx CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A MIIBCgKCAQEAhkv0Sr7ALfc58YrnLXzVGfTRg1T9xUfuZqhdu80BgHTfaJDLX66icHHRRoso/hho EIYo1pUQTq0DtgmqkLg9rAup3rR+pImfcHBC55+vMDoEf5t88H/i0qDn3r63PxeULRoFIkCX9aVG uUPDe2CHAxB1UXUxyDf7ZAdIQJLPJdOQlsNRleBBoek4vuo2ZHv+A2tbAhE8/rIoQlDvXSpCZ9P7 m9TrFOb7tB4pHjJjESdmqcnEFc5zepAT8IuRAGZ1OkjJs74JUp+03do8scTMXzvVi4jefpyXhnoN C0da4OwPig7UmbDsrSCGbqz29UgxmGUmSnLchpkglw1eET5hTwIDAQABMA0GCSqGSIb3DQEBCwUA A4IBAQAA5WBtCPlaSIm1NIpKYd2x8qfeKc2YsxbAPukgUFaRDl1uxGw1HdzNzUp9X4JOF/futpw/ yhmw9o1GHBukIdj0mJRt8O9szRdkJmx4EfbY5bTVzkQ7QGv9FI1LBD6z6KgJEOxEGpDbh2Z8uyW8 HvxXgZgiyan53FauVJe+UuAkBy2ynJcVKK3+vUEISFXn1oh5SPOmi+2R4WKSgyTqOKpuowHHHg9u EbwwnXPMU4q3QLG1oDrp0ZvVuprvJaoWd5zIt/TYB3Hb5oEO7Imwx1n9K9QskYmFygR9rdJ6VS7L 6/h6rcL/dKjm4pU0Dgk9h9Hi8ps7Mn+nRRhsWQbiD59n first.last@example.com https://signin.aws.amazon.com/saml first.last@example.com arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps 28800 urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified ================================================ FILE: aws_google_auth/u2f.py ================================================ #!/usr/bin/env python import json import time import requests from u2flib_host import u2f, exc, appid from u2flib_host.constants import APDU_USE_NOT_SATISFIED """ The facet/appID used by Google auth does not seem to be valid Need to apply some patches to u2flib_host to not validate the content type when fetching the appId and don't validate the returned facets (appID & facet list is hosted at https://www.gstatic.com/securitykey/origins.json which is not valid for the facet https://accounts.google.com) """ def __appid_verifier__fetch_json(app_id): target = app_id while True: resp = requests.get(target, allow_redirects=False, verify=True) # If the server returns an HTTP redirect (status code 3xx) the # server must also send the header "FIDO-AppID-Redirect-Authorized: # true" and the client must verify the presence of such a header # before following the redirect. This protects against abuse of # open redirectors within the target domain by unauthorized # parties. if 300 <= resp.status_code < 400: if resp.headers.get('FIDO-AppID-Redirect-Authorized') != \ 'true': raise ValueError('Redirect must set ' 'FIDO-AppID-Redirect-Authorized: true') target = resp.headers['location'] else: return resp.json() def __appid_verifier__valid_facets(app_id, facets): return facets def u2f_auth(challenges, facet): devices = u2f.list_devices() for device in devices[:]: try: device.open() except: # Some U2F devices fail on the first attempt to open but # succeed on subsequent attempts. So retry once. try: device.open() except: devices.remove(device) try: prompted = False while devices: removed = [] for device in devices: remove = True for challenge in challenges: try: return u2f.authenticate(device, json.dumps(challenge), facet) except exc.APDUError as e: if e.code == APDU_USE_NOT_SATISFIED: remove = False if not prompted: print('Touch the flashing U2F device to ' 'authenticate...') prompted = True else: pass except exc.DeviceError: pass if remove: removed.append(device) devices = [d for d in devices if d not in removed] for d in removed: d.close() time.sleep(0.25) finally: for device in devices: device.close() raise RuntimeWarning("U2F Device Not Found") appid.verifier.fetch_json = __appid_verifier__fetch_json appid.verifier.valid_facets = __appid_verifier__valid_facets ================================================ FILE: aws_google_auth/util.py ================================================ #!/usr/bin/env python from __future__ import print_function import getpass import os import sys from collections import OrderedDict from six.moves import input from tabulate import tabulate class Util: @staticmethod def get_input(prompt): return input(prompt) @staticmethod def pick_a_role(roles, aliases=None, account=None): if account: filtered_roles = {role: principal for role, principal in roles.items() if(account in role)} else: filtered_roles = roles if aliases: enriched_roles = {} for role, principal in filtered_roles.items(): enriched_roles[role] = [ aliases[role.split(':')[4]], role.split('role/')[1], principal ] enriched_roles = OrderedDict(sorted(enriched_roles.items(), key=lambda t: (t[1][0], t[1][1]))) ordered_roles = OrderedDict() for role, role_property in enriched_roles.items(): ordered_roles[role] = role_property[2] enriched_roles_tab = [] for i, (role, role_property) in enumerate(enriched_roles.items()): enriched_roles_tab.append([i + 1, role_property[0], role_property[1]]) while True: print(tabulate(enriched_roles_tab, headers=['No', 'AWS account', 'Role'], )) prompt = 'Type the number (1 - {:d}) of the role to assume: '.format(len(enriched_roles)) choice = Util.get_input(prompt) try: return list(ordered_roles.items())[int(choice) - 1] except (IndexError, ValueError): print("Invalid choice, try again.") else: while True: for i, role in enumerate(filtered_roles): print("[{:>3d}] {}".format(i + 1, role)) prompt = 'Type the number (1 - {:d}) of the role to assume: '.format(len(filtered_roles)) choice = Util.get_input(prompt) try: return list(filtered_roles.items())[int(choice) - 1] except (IndexError, ValueError): print("Invalid choice, try again.") @staticmethod def touch(file_name, mode=0o600): flags = os.O_CREAT | os.O_APPEND with os.fdopen(os.open(file_name, flags, mode)) as f: try: os.utime(file_name, None) finally: f.close() # This method returns the first non-None value in args. If all values are # None, None will be returned. If there are no arguments, None will be # returned. @staticmethod def coalesce(*args): for _, value in enumerate(args): if value is not None: return value return None @staticmethod def unicode_to_string_if_needed(object): if "unicode" in str(object.__class__): return object.encode('utf-8') else: return object @staticmethod def get_password(prompt): if sys.stdin.isatty(): password = getpass.getpass(prompt) else: print(prompt, end="") sys.stdout.flush() password = sys.stdin.readline() print("") return password ================================================ FILE: requirements.txt ================================================ beautifulsoup4 boto3 configparser filelock keyring lxml Pillow requests six tabulate tzlocal ================================================ FILE: setup.py ================================================ """A setuptools based setup module. See: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ # Always prefer setuptools over distutils from setuptools import setup, find_packages # To use a consistent encoding from codecs import open from os import path here = path.abspath(path.dirname(__file__)) # Get the long description from the README file with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() version = {} with open(path.join(here, 'aws_google_auth/_version.py')) as fp: exec(fp.read(), version) setup( name='aws-google-auth', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version=version['__version__'], description='Acquire AWS STS (temporary) credentials via Google Apps ' 'SAML Single Sign On', long_description=long_description, # The project's main homepage. url='https://github.com/cevoaustralia/aws-google-auth', download_url='https://github.com/cevoaustralia/aws-google-auth/archive/%s.tar.gz' % version['__version__'], # Author details author='Colin Panisset', author_email='colin.panisset@cevo.com.au', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 3 - Alpha', # Indicate who your project is intended for 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'Topic :: Security', 'Topic :: System :: Systems Administration :: Authentication/Directory', # Pick your license as you wish (should match "license" above) 'License :: OSI Approved :: MIT License', # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', ], # What does your project relate to? keywords='saml sso federated identity google aws cli', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Alternatively, if you want to distribute just a my_module.py, uncomment # this: # py_modules=["my_module"], # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html # install_requires=['peppercorn'], install_requires=[ 'beautifulsoup4', 'boto3', 'configparser', 'filelock', 'keyring', 'keyrings.alt', 'lxml', 'Pillow', 'requests', 'six', 'tabulate', 'tzlocal' ], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test] # extras_require={ # 'dev': ['check-manifest'], # 'test': ['coverage'], # }, extras_require={ 'u2f': ['python-u2flib-host'], }, # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. # package_data={ # 'sample': ['package_data.dat'], # }, # Although 'package_data' is the preferred approach, in some case you may # need to place data files outside of your packages. See: # http://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files # noqa # In this case, 'data_file' will be installed into '/my_data' # data_files=[('my_data', ['data/data_file'])], # To provide executable scripts, use entry points in preference to the # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. entry_points={ 'console_scripts': [ 'aws-google-auth=aws_google_auth:main', ], }, test_suite='nose.collector', tests_require=['nose', 'mock'], )