Showing preview only (240K chars total). Download the full file or copy to clipboard to get everything.
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 <https://aws.amazon.com/blogs/security/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 <https://blog.faisalmisle.com/2015/11/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 <https://github.com/cevoaustralia/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 <https://virtualenv.pypa.io/en/stable/>`__.
.. 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 <CODE_OF_CONDUCT.md>`__ and
`contributing <CONTRIBUTING.md>`__ 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 <https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_manage_modify.html>`__
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 <https://github.com/cevoaustralia/aws-google-auth/issues/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 <https://github.com/wheniwork/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 <https://github.com/venth/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, '<PASSWORD>'))
out.write(("\njson: " + json.dumps(json_data, indent=2)).replace(self.config.password, '<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
================================================
<!DOCTYPE doctype html>
<html dir="ltr" lang="en-GB"><head><base href="https://accounts.google.com/"/><script data-id="_gd" nonce="9p3KdyeCn8wQVycydUjMktBX+yg">window.WIZ_global_data = {"OewCAd":"%.@.\"xsrf\",\"AFoagUWhtXs5OmdKenUrLjSFuQG0jowDbg:1520434370879\",[\"115408737972073202986\"]\n,\"AFoagUWdx7XaQZaOh3hvreCOg9adUIHnVg:1520434370880\"]\n","thykhd":"AK49qIAhj1cMAsji87O-UTyEybikdELqBBl9S1aAmmQv3PeIgLXhSyBL5zL0Qc-t0U8Sbn_ovgQydcd6hgPBt_YhVmPYoJ7wH-cdOrsYqlHq1w\u003d\u003d","w2btAe":"%.@.null,null,\"\",false]\n"};</script><meta charset="utf-8"/><meta content="IE=edge" http-equiv="X-UA-Compatible"/><link href="//www.google.com/favicon.ico" rel="shortcut icon"/><script nonce="9p3KdyeCn8wQVycydUjMktBX+yg">(function(H) {H.className="CMgTXc";})(document.documentElement);</script><meta content="width=300, initial-scale=1" name="viewport"/><meta content="noindex" name="robots"/><meta content="LrdTUW9psUAMbh4Ia074-BPEVmcpBxF6Gwf0MSgQXZs" name="google-site-verification"/><title>Google Accounts</title><script nonce="9p3KdyeCn8wQVycydUjMktBX+yg" type="text/javascript">(function(){var f=this,aa=function(){},ba=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";
else if("function"==b&&"undefined"==typeof a.call)return"object";return b},h=Date.now||function(){return+new Date},t=function(a,b){function c(){}c.prototype=b.prototype;a.w=b.prototype;a.prototype=new c;a.prototype.constructor=a;a.u=function(a,c,g){for(var e=Array(arguments.length-2),d=2;d<arguments.length;d++)e[d-2]=arguments[d];return b.prototype[c].apply(a,e)}};var u=function(a){if(Error.captureStackTrace)Error.captureStackTrace(this,u);else{var b=Error().stack;b&&(this.stack=b)}a&&(this.message=String(a))};t(u,Error);u.prototype.name="CustomError";var v=function(a,b){a=a.split("%s");for(var c="",e=a.length-1,d=0;d<e;d++)c+=a[d]+(d<b.length?b[d]:"%s");u.call(this,c+a[e])};t(v,u);v.prototype.name="AssertionError";var ca=function(a,b,c){if(!a){var e="Assertion failed";if(b){e+=": "+b;var d=Array.prototype.slice.call(arguments,2)}throw new v(""+e,d||[]);}};var y={};var da=function(a,b){if(null===b)return!1;if("contains"in a&&1==b.nodeType)return a.contains(b);if("compareDocumentPosition"in a)return a==b||!!(a.compareDocumentPosition(b)&16);for(;b&&a!=b;)b=b.parentNode;return b==a};var ea=function(a,b){return function(c){c||(c=window.event);return b.call(a,c)}},z=function(a){a=a.target||a.srcElement;!a.getAttribute&&a.parentNode&&(a=a.parentNode);return a},A="undefined"!=typeof navigator&&/Macintosh/.test(navigator.userAgent),fa="undefined"!=typeof navigator&&!/Opera/.test(navigator.userAgent)&&/WebKit/.test(navigator.userAgent),ha={A:1,INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1},ia=function(a){return(a=a.changedTouches&&a.changedTouches[0]||a.touches&&a.touches[0])?{clientX:a.clientX,
clientY:a.clientY,screenX:a.screenX,screenY:a.screenY}:null},ma=function(a){var b={};b.originalEventType=a.type;b.type="click";for(var c in a){var e=a[c];"type"==c||"srcElement"==c||"function"==ba(e)||(b[c]=e)}b.timeStamp=h();b.defaultPrevented=!1;b.preventDefault=ka;b._propagationStopped=!1;b.stopPropagation=la;if(a=ia(a))b.clientX=a.clientX,b.clientY=a.clientY,b.screenX=a.screenX,b.screenY=a.screenY;return b},na=function(){this._mouseEventsPrevented=!0},ka=function(){this.defaultPrevented=!0},la=
function(){this._propagationStopped=!0},oa={A:13,BUTTON:0,CHECKBOX:32,COMBOBOX:13,GRIDCELL:13,LINK:13,LISTBOX:13,MENU:0,MENUBAR:0,MENUITEM:0,MENUITEMCHECKBOX:0,MENUITEMRADIO:0,OPTION:0,RADIO:32,RADIOGROUP:32,RESET:0,SUBMIT:0,TAB:0,TREE:13,TREEITEM:13},qa=function(a){return(a.getAttribute("type")||a.tagName).toUpperCase()in pa},sa=function(a){return(a.getAttribute("type")||a.tagName).toUpperCase()in ra},pa={CHECKBOX:!0,OPTION:!0,RADIO:!0},ra={COLOR:!0,DATE:!0,DATETIME:!0,"DATETIME-LOCAL":!0,EMAIL:!0,
MONTH:!0,NUMBER:!0,PASSWORD:!0,RANGE:!0,SEARCH:!0,TEL:!0,TEXT:!0,TEXTAREA:!0,TIME:!0,URL:!0,WEEK:!0},ta={A:!0,AREA:!0,BUTTON:!0,DIALOG:!0,IMG:!0,INPUT:!0,LINK:!0,MENU:!0,OPTGROUP:!0,OPTION:!0,PROGRESS:!0,SELECT:!0,TEXTAREA:!0};var ua=function(){this.m=[];this.a=[];this.i=[];this.l={};this.c=null;this.j=[];this.s=aa},B,va,wa="undefined"!=typeof navigator&&/iPhone|iPad|iPod/.test(navigator.userAgent),xa=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^\s+/,"").replace(/\s+$/,"")},ya=/\s*;\s*/,C=null,Ba=function(a,b){return function(c){var e=b;if("click"==e&&(A&&c.metaKey||!A&&c.ctrlKey||2==c.which||null==c.which&&4==c.button||"auxclick"==c.type||c.shiftKey))e="clickmod";else{var d=c.which||
c.keyCode||c.key;fa&&3==d&&(d=13);if(13!=d&&32!=d)d=!1;else{var g=z(c);var k=(g.getAttribute("role")||g.type||g.tagName).toUpperCase();var m;(m="keydown"!=c.type)||("getAttribute"in g?(m=(g.getAttribute("role")||g.tagName).toUpperCase(),m=!sa(g)&&("COMBOBOX"!=m||"INPUT"!=m)&&!g.isContentEditable):m=!1,m=!m);(m=m||c.ctrlKey||c.shiftKey||c.altKey||c.metaKey||qa(g)&&32==d)||((m=g.tagName in ha)||(m=g.getAttributeNode("tabindex"),m=null!=m&&m.specified),m=!(m&&!g.disabled));m?d=!1:(g="INPUT"!=g.tagName.toUpperCase()||
g.type,m=!(k in oa)&&13==d,d=(0==oa[k]%d||m)&&!!g)}d&&(e="clickkey")}g=c.srcElement||c.target;d=D(e,c,g,"",null);for(k=g;k&&k!=this;k=k.__owner||k.parentNode){var l=k;b:{var w,r=l;var n=e;m=c;var p=r.__jsaction;if(!p){p=null;"getAttribute"in r&&(p=r.getAttribute("jsaction"));if(w=p){if(p=y[w],!p){p={};for(var K=w.split(ya),L=0,Ga=K?K.length:0;L<Ga;L++){var x=K[L];if(x){var M=x.indexOf(":"),ja=-1!=M,Ha=ja?xa(x.substr(0,M)):"click";x=ja?xa(x.substr(M+1)):x;p[Ha]=x}}y[w]=p}}else p=za;r.__jsaction=p}"clickkey"==
n?n="click":"click"!=n||p.click||(n="clickonly");w=null;if(p.click){r=Aa(r,m,p);if(!r){n={f:n,action:"",event:null,o:!0};break b}r!=m&&(w=r,n=r.type)}n={f:n,action:p[n]||"",event:w,o:!1}}if(n.o||n.action)break}n&&(d=D(n.f,n.event||c,g,n.action||"",l,d.timeStamp));d&&"touchend"==d.eventType&&(d.event._preventMouseEvents=na);if(n&&n.action){if(k="clickkey"==e)k=z(c),k=(k.type||k.tagName).toUpperCase(),(k=32==(c.which||c.keyCode||c.key)&&"CHECKBOX"!=k)||(k=z(c),g=(k.getAttribute("role")||k.tagName).toUpperCase(),
k=k.tagName.toUpperCase()in ta&&"A"!=g&&!qa(k)&&!sa(k)||"BUTTON"==g);k&&(c.preventDefault?c.preventDefault():c.returnValue=!1);if("mouseenter"==e||"mouseleave"==e)if(k=c.relatedTarget,!("mouseover"==c.type&&"mouseenter"==e||"mouseout"==c.type&&"mouseleave"==e)||k&&(k===l||da(l,k)))d.action="",d.actionElement=null;else{e={};for(var q in c)"function"!==typeof c[q]&&"srcElement"!==q&&"target"!==q&&(e[q]=c[q]);e.type="mouseover"==c.type?"mouseenter":"mouseleave";e.target=e.srcElement=l;e.bubbles=!1;d.event=
e;d.targetElement=l}}else d.action="",d.actionElement=null;l=d;a.c&&(q=D(l.eventType,l.event,l.targetElement,l.action,l.actionElement,l.timeStamp),"clickonly"==q.eventType&&(q.eventType="click"),a.c(q,!0));if(l.actionElement){"A"!=l.actionElement.tagName||"click"!=l.eventType&&"clickmod"!=l.eventType||l.actionElement.hasAttribute("data-unjs")&&null==a.c||(c.preventDefault?c.preventDefault():c.returnValue=!1);if(a.c)a.c(l);else{a.s(l);if((q=f.document)&&!q.createEvent&&q.createEventObject)try{var N=
q.createEventObject(c)}catch(Pa){N=c}else N=c;l.event=N;a.j.push(l)}"touchend"==l.event.type&&l.event._mouseEventsPrevented&&(C=ma(l.event))}}},D=function(a,b,c,e,d,g){return{eventType:a,event:b,targetElement:c,action:e,actionElement:d,timeStamp:g||h()}},za={},Aa=function(a,b,c){if("click"==b.type||b.targetTouches&&1<b.targetTouches.length)return b;var e=B,d=b.target;if(d&&Ca(d))return b;d=ia(b);if("touchstart"!=b.type||c.touchstart||c.touchend)if("touchend"==b.type&&e&&e.node==a)if(b.defaultPrevented||
d&&4<Math.abs(d.clientX-e.x)+Math.abs(d.clientY-e.y))B=null;else{C=a=ma(b);b.stopPropagation();b.preventDefault();document.createEvent?(b=document.createEvent("MouseEvent"),b.initMouseEvent(a.type,!0,!0,window,a.detail||1,a.screenX||0,a.screenY||0,a.clientX||0,a.clientY||0,a.ctrlKey||!1,a.altKey||!1,a.shiftKey||!1,a.metaKey||!1,a.button||0,a.relatedTarget||null)):(ca(document.createEventObject),b=document.createEventObject(),b.type=a.type,b.clientX=a.clientX,b.clientY=a.clientY,b.button=a.button,
b.detail=a.detail,b.ctrlKey=a.ctrlKey,b.altKey=a.altKey,b.shiftKey=a.shiftKey,b.metaKey=a.metaKey);b.v=a.timeStamp;b._fastclick=!0;a.target.dispatchEvent(b);if(!b.defaultPrevented){if(document.activeElement&&document.activeElement!=b.target&&Ca(document.activeElement))try{document.activeElement.blur()}catch(g){}try{window.getSelection().removeAllRanges()}catch(g){}}return null}else"touchmove"==b.type&&e&&d&&4<Math.abs(d.clientX-e.x)+Math.abs(d.clientY-e.y)&&(B=null);else return B={node:a,x:d?d.clientX:
0,y:d?d.clientY:0},C=null,clearTimeout(va),va=setTimeout(Da,400),null;return b},Ca=function(a){a=a.tagName||"";return"TEXTAREA"==a||"INPUT"==a||"SELECT"==a||"OPTION"==a},Da=function(){B=null},E=function(a){if(!a._fastclick){var b=C;if(b)if(800<h()-b.timeStamp)C=null;else{var c=4>=Math.abs(a.clientX-b.clientX)+Math.abs(a.clientY-b.clientY);b.target==a.target||c?(a.stopPropagation(),a.preventDefault(),"click"==a.type&&(C=null)):C=null}}},Ea=function(a,b){return function(c){var e=a,d=b,g=!1;"mouseenter"==
e?e="mouseover":"mouseleave"==e&&(e="mouseout");if(c.addEventListener){if("focus"==e||"blur"==e||"error"==e||"load"==e)g=!0;c.addEventListener(e,d,g)}else c.attachEvent&&("focus"==e?e="focusin":"blur"==e&&(e="focusout"),d=ea(c,d),c.attachEvent("on"+e,d));return{f:e,g:d,capture:g}}},F=function(a,b){if(!a.l.hasOwnProperty(b)){var c=Ba(a,b),e=Ea(b,c);a.l[b]=c;a.m.push(e);for(c=0;c<a.a.length;++c){var d=a.a[c];d.h.push(e.call(null,d.b))}"click"==b&&F(a,"keydown");"click"==b&&(F(a,"touchstart"),F(a,"touchend"),
F(a,"touchmove"),document.addEventListener&&(document.addEventListener("click",E,!0),document.addEventListener("mouseup",E,!0),document.addEventListener("mousedown",E,!0)))}};ua.prototype.g=function(a){return this.l[a]};var Fa=function(a){var b=G,c=a.b;wa&&(c.style.cursor="pointer");for(c=0;c<b.m.length;++c)a.h.push(b.m[c].call(null,a.b))},Ja=function(){this.b=Ia;this.h=[]};Ja.prototype.containsNode=function(a){for(var b=this.b;b!=a&&a.parentNode;)a=a.parentNode;return b==a};
var La=function(){for(var a=H,b=Ka,c=0;c<b.length;++c)if(b[c].b!=a.b&&b[c].containsNode(a.b))return!0;return!1};var Ma=window,G=new ua,Na=Ma||window,Ia=Na.document.documentElement,I=new Ja,J;a:{for(var O=0;O<G.a.length;O++)if(G.a[O].containsNode(Ia)){J=!0;break a}J=!1}
if(J)G.i.push(I);else{Fa(I);G.a.push(I);for(var Ka=G.i.concat(G.a),P=[],Q=[],R=0;R<G.a.length;++R){var H=G.a[R];if(La()){P.push(H);for(var S=H,T=0;T<S.h.length;++T){var U=S.b,V=S.h[T];U.removeEventListener?U.removeEventListener(V.f,V.g,V.capture):U.detachEvent&&U.detachEvent("on"+V.f,V.g)}S.h=[]}else Q.push(H)}for(R=0;R<G.i.length;++R)H=G.i[R],La()?P.push(H):(Q.push(H),Fa(H));G.a=Q;G.i=P}F(G,"blur");F(G,"click");F(G,"focus");F(G,"focusin");F(G,"focusout");F(G,"keydown");F(G,"keypress");F(G,"load");
F(G,"mouseover");F(G,"mouseout");F(G,"mouseenter");F(G,"mouseleave");F(G,"submit");F(G,"touchstart");F(G,"touchend");F(G,"touchmove");F(G,"change");F(G,"input");F(G,"keyup");F(G,"mousedown");F(G,"mouseup");F(G,"touchcancel");F(G,"transitionend");F(G,"webkitTransitionEnd");
var Oa=function(a){return{trigger:function(b){var c=a.g(b.type);c||(F(a,b.type),c=a.g(b.type));var e=b.target||b.srcElement;c&&c.call(e.ownerDocument.documentElement,b)},bind:function(b){a.c=b;a.j&&(0<a.j.length&&b(a.j),a.j=null)}}}(G),W=["ID_wizbind"],X=Na||f;W[0]in X||!X.execScript||X.execScript("var "+W[0]);for(var Y;W.length&&(Y=W.shift());){var Z;if(Z=!W.length)Z=void 0!==Oa;Z?X[Y]=Oa:X=X[Y]&&X[Y]!==Object.prototype[Y]?X[Y]:X[Y]={}};}).call(this);
</script><style nonce="9p3KdyeCn8wQVycydUjMktBX+yg">@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'), url(//fonts.gstatic.com/s/opensans/v15/mem5YaGs126MiZpBA-UN_r8OUuhs.ttf) format('truetype');
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans'), local('OpenSans'), url(//fonts.gstatic.com/s/opensans/v15/mem8YaGs126MiZpBA-UFVZ0e.ttf) format('truetype');
}
</style><link href="https://ssl.gstatic.com/accounts/static/_/ss/k=gaia.gaiafe_signin.-h1ry6r4lwuid.L.X.O/am=AggAMCAgQAQE/d=0/rs=ABkqax0IgeKB4xoA19vYnT-DABJacU0iPw" rel="stylesheet" type="text/css"/><script nonce="9p3KdyeCn8wQVycydUjMktBX+yg" type="text/javascript">window['cssLoaded'] = true;</script><script async="" id="base-js" nonce="9p3KdyeCn8wQVycydUjMktBX+yg" src="https://ssl.gstatic.com/accounts/static/_/js/k=gaia.gaiafe_signin.en_GB.ER1odfw2bO0.O/m=signin,signin_challenge/am=AggAMCAgQAQE/rt=j/d=1/rs=ABkqax1iLdk0B7wFQ6oZPZTTm9w9v1nySg"></script><script nonce="9p3KdyeCn8wQVycydUjMktBX+yg">var AF_initDataKeys = ["ds:0"]
; var AF_dataServiceRequests = {'ds:0' : {id: 1.02163051E8 }}; var AF_initDataChunkQueue = []; var AF_initDataCallback; var AF_initDataInitializeCallback; if (AF_initDataInitializeCallback) {AF_initDataInitializeCallback(AF_initDataKeys, AF_initDataChunkQueue, AF_dataServiceRequests);}if (!AF_initDataCallback) {AF_initDataCallback = function(chunk) {AF_initDataChunkQueue.push(chunk);};}</script></head><body id="yDmH0d"><div class="s2h6df"><div class="JYXKFb IA6off"><div class="ql1pVb ZnXjYc EaNIqc"><div aria-label="Google" class="omTHz"></div></div></div><div class="RgEUV ZnXjYc EaNIqc JhUD8d"><div><div class="glT6eb"><div jsname="IDL96d"><h1>2-step Verification</h1></div><div jsname="jqgtP"><h2>This extra step shows that it’s really you trying to sign in</h2></div></div></div><div class="H9T9of" id="contactAdminMessage"><span class="y0GOlc">Based on your organisation's policy, you need to turn on 2-step verification. Contact your administrator to learn more.</span></div><div class="LJtPoc" jsname="Ki8mld"><form action="/signin/challenge/bc/2" id="challenge" jsaction="submit:zbvklb" jscontroller="HNBfvc" jsname="rzWj5" jsshadow="" method="POST"><content><input id="challengeId" name="challengeId" type="hidden" value="2"/><input id="challengeType" name="challengeType" type="hidden" value="8"/><input name="continue" type="hidden" value="https://accounts.google.com/o/saml2/initsso?idpid=XXXxxxxxx&spid=999999999999&forceauthn=false&from_login=1&as=T-8dnNYIaIinGuuiq2MmaA"/><input name="scc" type="hidden" value="1"/><input name="sarp" type="hidden" value="1"/><input name="checkedDomains" type="hidden" value="youtube"/><input name="pstMsg" type="hidden" value="0"/><input name="TL" type="hidden" value="AHnYQLx2YxQ8tVTnEjACd3aRQ8wKufH6IIBbKOiEITcE7wEHgobxP2EmCPXQUmbYS4AXE-h7l8D29s8FC8lfYrop4xrF6GoGPBIQQB9yDX51OCF9ox9P85sjJxo0Pbbj4NrccrHP97q7JNa9RcBXtgHE1V3H1QZC9Q"/><input id="gxf" name="gxf" type="hidden" value="AFoagUWLOsdTw98_m59Nn2uKJomKbqpD-w:1520434370876"/><div jsname="KrwUDc"><img alt="" class="JC07Dd" jsname="TqVmm" src="//ssl.gstatic.com/accounts/marc/backup_code.png"/><div class="EGmPD" jsname="BCqkPb">Enter a backup code</div><div class="VnJmLc" jsname="NhJ5Dd">Enter one of the backup codes you received from Google.</div><div class="gIH97b"><input autocomplete="off" autofocus="" class="y1x0pc" dir="ltr" id="backupCodePin" name="Pin" pattern="[0-9 ]*" placeholder="Enter the 8-digit code" type="tel"/></div><input class="MK9CEd MVpUfe" id="submit" jsaction="aJAbCd:zbvklb" jscontroller="rrJN5c" jsname="M2UYVd" type="submit" value="Done"/><div class="ARshqb"><input checked="" class="aCOJmf" id="trustDevice" name="TrustDevice" type="checkbox"/><span>Remember this computer for 30 days</span><div class="Bfmfyc" role="tooltip"><div class="x7qQqf"></div><div class="hzC8Lb">For your convenience, keep this ticked. On shared devices, additional precautions are recommended. <a href="https://support.google.com/accounts/?p=securesignin&hl=en_GB" target="_blank">Learn more</a></div></div></div></div></content></form></div><div class=" KSYbxc "><form action="/signin/challenge/skip" method="POST"><input name="challengeId" type="hidden" value="2"/><input name="continue" type="hidden" value="https://accounts.google.com/o/saml2/initsso?idpid=XXXxxxxxx&spid=999999999999&forceauthn=false&from_login=1&as=T-8dnNYIaIinGuuiq2MmaA"/><input name="scc" type="hidden" value="1"/><input name="sarp" type="hidden" value="1"/><input name="checkedDomains" type="hidden" value="youtube"/><input name="pstMsg" type="hidden" value="0"/><input name="TL" type="hidden" value="AHnYQLx2YxQ8tVTnEjACd3aRQ8wKufH6IIBbKOiEITcE7wEHgobxP2EmCPXQUmbYS4AXE-h7l8D29s8FC8lfYrop4xrF6GoGPBIQQB9yDX51OCF9ox9P85sjJxo0Pbbj4NrccrHP97q7JNa9RcBXtgHE1V3H1QZC9Q"/><input id="gxf" name="gxf" type="hidden" value="AFoagUWLOsdTw98_m59Nn2uKJomKbqpD-w:1520434370876"/><input class="g1C42c" id="skipChallenge" jsname="rwR6T" type="submit" value="Try another way to sign in"/></form></div><div class="M0leCe"><span jsname="tODuDc">test@test.com</span><a class="vHOx3b" href="https://accounts.google.com/AccountChooser?continue=https%3A%2F%2Faccounts.google.com%2Fo%2Fsaml2%2Finitsso%3Fidpid%3DXXXxxxxxx%26spid%3D999999999999%26forceauthn%3Dfalse%26from_login%3D1%26as%3DT-8dnNYIaIinGuuiq2MmaA&sarp=1">Use a different account</a></div></div><div class="zOB73"><div class="SEK88d ZnXjYc EaNIqc"><ul id="footer-list"><li>Google</li><li><a href="https://accounts.google.com/TOS?loc=IE&hl=en-GB&privacy=true" target="_blank">Privacy</a></li><li><a href="https://accounts.google.com/TOS?loc=IE&hl=en-GB" target="_blank">Terms</a></li></ul><div id="lang-vis-control" jsaction="change:iktSbe" jscontroller="fBrDlb"><span class="KQh9Y" id="lang-chooser-wrap"><label for="lang-chooser"><img alt="Change language" src="//ssl.gstatic.com/images/icons/ui/common/universal_language_settings-21.png"/></label><select class="BTDeVb" id="lang-chooser" jsname="J2uaq" name="lang-chooser"><option value="af">Afrikaans</option><option value="az">azərbaycan</option><option value="ca">català</option><option value="cs">Čeština</option><option value="da">Dansk</option><option value="de">Deutsch</option><option value="et">eesti</option><option selected="selected" value="en-GB">English (United Kingdom)</option><option value="en">English (United States)</option><option value="es">Español (España)</option><option value="es-419">Español (Latinoamérica)</option><option value="eu">euskara</option><option value="fil">Filipino</option><option value="fr-CA">Français (Canada)</option><option value="fr">Français (France)</option><option value="gl">galego</option><option value="hr">Hrvatski</option><option value="in">Indonesia</option><option value="zu">isiZulu</option><option value="is">íslenska</option><option value="it">Italiano</option><option value="sw">Kiswahili</option><option value="lv">latviešu</option><option value="lt">lietuvių</option><option value="hu">magyar</option><option value="ms">Melayu</option><option value="nl">Nederlands</option><option value="no">norsk</option><option value="pl">polski</option><option value="pt">Português (Brasil)</option><option value="pt-PT">Português (Portugal)</option><option value="ro">română</option><option value="sk">Slovenčina</option><option value="sl">slovenščina</option><option value="fi">Suomi</option><option value="sv">Svenska</option><option value="vi">Tiếng Việt</option><option value="tr">Türkçe</option><option value="el">Ελληνικά</option><option value="bg">български</option><option value="mn">монгол</option><option value="ru">Русский</option><option value="sr">српски</option><option value="uk">Українська</option><option value="ka">ქართული</option><option value="hy">հայերեն</option><option value="iw">עברית</option><option value="ur">اردو</option><option value="ar">العربية</option><option value="fa">فارسی</option><option value="am">አማርኛ</option><option value="ne">नेपाली</option><option value="mr">मराठी</option><option value="hi">हिन्दी</option><option value="bn">বাংলা</option><option value="gu">ગુજરાતી</option><option value="ta">தமிழ்</option><option value="te">తెలుగు</option><option value="kn">ಕನ್ನಡ</option><option value="ml">മലയാളം</option><option value="si">සිංහල</option><option value="th">ไทย</option><option value="lo">ລາວ</option><option value="my">မြန်မာ</option><option value="km">ខ្មែរ</option><option value="ko">한국어</option><option value="zh-HK">中文(香港)</option><option value="ja">日本語</option><option value="zh-CN">简体中文</option><option value="zh-TW">繁體中文</option></select></span></div></div></div></div></body></html><div class="lDwpOe"></div><script nonce="9p3KdyeCn8wQVycydUjMktBX+yg">AF_initDataCallback({key: 'ds:0', isError: false , hash: '1', data:function(){return [[[["Afghanistan (\u202bافغانستان\u202c\u200e)","AF"]
,["Åland Islands (Åland)","AX"]
,["Albania (Shqipëri)","AL"]
,["Algeria","DZ"]
,["American Samoa","AS"]
,["Andorra","AD"]
,["Angola","AO"]
,["Anguilla","AI"]
,["Antigua \u0026 Barbuda","AG"]
,["Argentina","AR"]
,["Armenia (Հայաստան)","AM"]
,["Aruba","AW"]
,["Australia","AU"]
,["Austria (Österreich)","AT"]
,["Azerbaijan (Azərbaycan)","AZ"]
,["Bahamas","BS"]
,["Bahrain (\u202bالبحرين\u202c\u200e)","BH"]
,["Bangladesh (বাংলাদেশ)","BD"]
,["Barbados","BB"]
,["Belarus (Беларусь)","BY"]
,["Belgium","BE"]
,["Belize","BZ"]
,["Benin (Bénin)","BJ"]
,["Bermuda","BM"]
,["Bhutan (འབྲུག)","BT"]
,["Bolivia","BO"]
,["Bosnia \u0026 Herzegovina (Bosna i Hercegovina)","BA"]
,["Botswana","BW"]
,["Brazil (Brasil)","BR"]
,["British Indian Ocean Territory","IO"]
,["British Virgin Islands","VG"]
,["Brunei","BN"]
,["Bulgaria (България)","BG"]
,["Burkina Faso","BF"]
,["Burundi (Uburundi)","BI"]
,["Cambodia (កម្ពុជា)","KH"]
,["Cameroon (Cameroun)","CM"]
,["Canada","CA"]
,["Cape Verde (Kabu Verdi)","CV"]
,["Caribbean Netherlands","BQ"]
,["Cayman Islands","KY"]
,["Central African Republic (République centrafricaine)","CF"]
,["Chad (Tchad)","TD"]
,["Chile","CL"]
,["China (中国)","CN"]
,["Christmas Island","CX"]
,["Cocos (Keeling) Islands (Kepulauan Cocos (Keeling))","CC"]
,["Colombia","CO"]
,["Comoros (\u202bجزر القمر\u202c\u200e)","KM"]
,["Congo - Brazzaville (Congo-Brazzaville)","CG"]
,["Congo - Kinshasa (Jamhuri ya Kidemokrasia ya Kongo)","CD"]
,["Cook Islands","CK"]
,["Costa Rica","CR"]
,["Côte d’Ivoire","CI"]
,["Croatia (Hrvatska)","HR"]
,["Cuba","CU"]
,["Curaçao","CW"]
,["Cyprus (Κύπρος)","CY"]
,["Czechia (Česko)","CZ"]
,["Denmark (Danmark)","DK"]
,["Djibouti","DJ"]
,["Dominica","DM"]
,["Dominican Republic (República Dominicana)","DO"]
,["Ecuador","EC"]
,["Egypt (\u202bمصر\u202c\u200e)","EG"]
,["El Salvador","SV"]
,["Equatorial Guinea (Guinea Ecuatorial)","GQ"]
,["Eritrea (ኤርትራ)","ER"]
,["Estonia (Eesti)","EE"]
,["Ethiopia","ET"]
,["Falkland Islands (Islas Malvinas)","FK"]
,["Faroe Islands (Føroyar)","FO"]
,["Fiji","FJ"]
,["Finland (Suomi)","FI"]
,["France","FR"]
,["French Guiana (Guyane française)","GF"]
,["French Polynesia (Polynésie française)","PF"]
,["Gabon","GA"]
,["Gambia","GM"]
,["Georgia (საქართველო)","GE"]
,["Germany (Deutschland)","DE"]
,["Ghana (Gaana)","GH"]
,["Gibraltar","GI"]
,["Greece (Ελλάδα)","GR"]
,["Greenland (Kalaallit Nunaat)","GL"]
,["Grenada","GD"]
,["Guadeloupe","GP"]
,["Guam","GU"]
,["Guatemala","GT"]
,["Guernsey","GG"]
,["Guinea (Guinée)","GN"]
,["Guinea-Bissau (Guiné-Bissau)","GW"]
,["Guyana","GY"]
,["Haiti","HT"]
,["Honduras","HN"]
,["Hong Kong (香港)","HK"]
,["Hungary (Magyarország)","HU"]
,["Iceland (Ísland)","IS"]
,["India (भारत)","IN"]
,["Indonesia","ID"]
,["Iran (\u202bایران\u202c\u200e)","IR"]
,["Iraq (\u202bالعراق\u202c\u200e)","IQ"]
,["Ireland","IE"]
,["Isle of Man","IM"]
,["Israel (\u202bישראל\u202c\u200e)","IL"]
,["Italy (Italia)","IT"]
,["Jamaica","JM"]
,["Japan (日本)","JP"]
,["Jersey","JE"]
,["Jordan (\u202bالأردن\u202c\u200e)","JO"]
,["Kazakhstan (Казахстан)","KZ"]
,["Kenya","KE"]
,["Kiribati","KI"]
,["Kuwait (\u202bالكويت\u202c\u200e)","KW"]
,["Kyrgyzstan (Кыргызстан)","KG"]
,["Laos (ລາວ)","LA"]
,["Latvia (Latvija)","LV"]
,["Lebanon (\u202bلبنان\u202c\u200e)","LB"]
,["Lesotho","LS"]
,["Liberia","LR"]
,["Libya (\u202bليبيا\u202c\u200e)","LY"]
,["Liechtenstein","LI"]
,["Lithuania (Lietuva)","LT"]
,["Luxembourg","LU"]
,["Macau (澳門)","MO"]
,["Macedonia (FYROM) (Република Македонија)","MK"]
,["Madagascar (Madagasikara)","MG"]
,["Malawi","MW"]
,["Malaysia","MY"]
,["Maldives","MV"]
,["Mali","ML"]
,["Malta","MT"]
,["Marshall Islands","MH"]
,["Martinique","MQ"]
,["Mauritania (\u202bموريتانيا\u202c\u200e)","MR"]
,["Mauritius (Moris)","MU"]
,["Mayotte","YT"]
,["Mexico (México)","MX"]
,["Micronesia","FM"]
,["Moldova (Republica Moldova)","MD"]
,["Monaco","MC"]
,["Mongolia (Монгол)","MN"]
,["Montenegro (Crna Gora)","ME"]
,["Montserrat","MS"]
,["Morocco","MA"]
,["Mozambique (Moçambique)","MZ"]
,["Myanmar (Burma) (မြန်မာ)","MM"]
,["Namibia (Namibië)","NA"]
,["Nauru","NR"]
,["Nepal (नेपाल)","NP"]
,["Netherlands (Nederland)","NL"]
,["New Caledonia (Nouvelle-Calédonie)","NC"]
,["New Zealand","NZ"]
,["Nicaragua","NI"]
,["Niger (Nijar)","NE"]
,["Nigeria","NG"]
,["Niue","NU"]
,["Norfolk Island","NF"]
,["Northern Mariana Islands","MP"]
,["North Korea (북한)","KP"]
,["Norway (Norge)","NO"]
,["Oman (\u202bعُمان\u202c\u200e)","OM"]
,["Pakistan (\u202bپاکستان\u202c\u200e)","PK"]
,["Palau","PW"]
,["Palestine (\u202bفلسطين\u202c\u200e)","PS"]
,["Panama (Panamá)","PA"]
,["Papua New Guinea","PG"]
,["Paraguay","PY"]
,["Peru (Perú)","PE"]
,["Philippines","PH"]
,["Poland (Polska)","PL"]
,["Portugal","PT"]
,["Puerto Rico","PR"]
,["Qatar (\u202bقطر\u202c\u200e)","QA"]
,["Réunion (La Réunion)","RE"]
,["Romania (România)","RO"]
,["Russia (Россия)","RU"]
,["Rwanda (U Rwanda)","RW"]
,["Samoa","WS"]
,["San Marino","SM"]
,["São Tomé \u0026 Príncipe (São Tomé e Príncipe)","ST"]
,["Saudi Arabia (\u202bالمملكة العربية السعودية\u202c\u200e)","SA"]
,["Senegal (Senegaal)","SN"]
,["Serbia (Србија)","RS"]
,["Seychelles","SC"]
,["Sierra Leone","SL"]
,["Singapore","SG"]
,["Sint Maarten","SX"]
,["Slovakia (Slovensko)","SK"]
,["Slovenia (Slovenija)","SI"]
,["Solomon Islands","SB"]
,["Somalia (Soomaaliya)","SO"]
,["South Africa","ZA"]
,["South Korea (대한민국)","KR"]
,["South Sudan (\u202bجنوب السودان\u202c\u200e)","SS"]
,["Spain (España)","ES"]
,["Sri Lanka (ශ්\u200dරී ලංකාව)","LK"]
,["St Helena (St. Helena)","SH"]
,["St Kitts \u0026 Nevis (St. Kitts \u0026 Nevis)","KN"]
,["St Lucia (St. Lucia)","LC"]
,["St Martin (Saint-Martin)","MF"]
,["St Pierre \u0026 Miquelon (Saint-Pierre-et-Miquelon)","PM"]
,["St Vincent \u0026 Grenadines (St. Vincent \u0026 Grenadines)","VC"]
,["Sudan (\u202bالسودان\u202c\u200e)","SD"]
,["Suriname","SR"]
,["Svalbard \u0026 Jan Mayen (Svalbard og Jan Mayen)","SJ"]
,["Swaziland","SZ"]
,["Sweden (Sverige)","SE"]
,["Switzerland (Schweiz)","CH"]
,["Syria (\u202bسوريا\u202c\u200e)","SY"]
,["Taiwan (台灣)","TW"]
,["Tajikistan (Тоҷикистон)","TJ"]
,["Tanzania","TZ"]
,["Thailand (ไทย)","TH"]
,["Timor-Leste","TL"]
,["Togo","TG"]
,["Tokelau","TK"]
,["Tonga","TO"]
,["Trinidad \u0026 Tobago","TT"]
,["Tunisia","TN"]
,["Turkey (Türkiye)","TR"]
,["Turkmenistan","TM"]
,["Turks \u0026 Caicos Islands","TC"]
,["Tuvalu","TV"]
,["Uganda","UG"]
,["Ukraine (Україна)","UA"]
,["United Arab Emirates (\u202bالإمارات العربية المتحدة\u202c\u200e)","AE"]
,["United Kingdom","GB"]
,["United States","US"]
,["Uruguay","UY"]
,["US Virgin Islands (U.S. Virgin Islands)","VI"]
,["Uzbekistan (Oʻzbekiston)","UZ"]
,["Vanuatu","VU"]
,["Vatican City (Città del Vaticano)","VA"]
,["Venezuela","VE"]
,["Vietnam (Việt Nam)","VN"]
,["Wallis \u0026 Futuna","WF"]
,["Yemen (\u202bاليمن\u202c\u200e)","YE"]
,["Zambia","ZM"]
,["Zimbabwe","ZW"]
]
]
]
}});</script>
================================================
FILE: aws_google_auth/tests/saml-response-expired-before-valid.xml
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://signin.aws.amazon.com/saml" ID="_7c434be06bf79a781dae9e7ed0024679" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_b1dd2c0469d905dfb1e10751d6feae95" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_b1dd2c0469d905dfb1e10751d6feae95">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+
VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj
iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0
SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu
bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509SubjectName>ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc.</ds:X509SubjectName>
<ds:X509Certificate>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</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">first.last@example.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotOnOrAfter="2017-07-24T10:36:41.125Z" Recipient="https://signin.aws.amazon.com/saml"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<!-- This will always be invalid for tests, the NotBefore is after the NotOnOrAfter -->
<saml2:Conditions NotBefore="2100-07-24T10:26:41.125Z" NotOnOrAfter="2011-07-24T10:36:41.125Z">
<saml2:AudienceRestriction>
<saml2:Audience>https://signin.aws.amazon.com/saml</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AttributeStatement>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">first.last@example.com</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">28800</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
<saml2:AuthnStatement AuthnInstant="2017-07-24T10:31:38.000Z" SessionIndex="_b1dd2c0469d905dfb1e10751d6feae95">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>
================================================
FILE: aws_google_auth/tests/saml-response-no-expire.xml
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://signin.aws.amazon.com/saml" ID="_7c434be06bf79a781dae9e7ed0024679" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_b1dd2c0469d905dfb1e10751d6feae95" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_b1dd2c0469d905dfb1e10751d6feae95">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+
VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj
iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0
SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu
bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509SubjectName>ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc.</ds:X509SubjectName>
<ds:X509Certificate>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</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">first.last@example.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotOnOrAfter="2017-07-24T10:36:41.125Z" Recipient="https://signin.aws.amazon.com/saml"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<!-- This will always be valid for tests, hence the very far off NotOnOrAfter -->
<saml2:Conditions NotBefore="2010-07-24T10:26:41.125Z" NotOnOrAfter="2100-07-24T10:36:41.125Z">
<saml2:AudienceRestriction>
<saml2:Audience>https://signin.aws.amazon.com/saml</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AttributeStatement>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">first.last@example.com</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">28800</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
<saml2:AuthnStatement AuthnInstant="2017-07-24T10:31:38.000Z" SessionIndex="_b1dd2c0469d905dfb1e10751d6feae95">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>
================================================
FILE: aws_google_auth/tests/saml-response-too-late.xml
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://signin.aws.amazon.com/saml" ID="_7c434be06bf79a781dae9e7ed0024679" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_b1dd2c0469d905dfb1e10751d6feae95" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_b1dd2c0469d905dfb1e10751d6feae95">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+
VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj
iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0
SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu
bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509SubjectName>ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc.</ds:X509SubjectName>
<ds:X509Certificate>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</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">first.last@example.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotOnOrAfter="2017-07-24T10:36:41.125Z" Recipient="https://signin.aws.amazon.com/saml"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<!-- This will always be invalid for tests, hence the very far off NotBefore and NotOnOrAfter -->
<saml2:Conditions NotBefore="2010-07-24T10:26:41.125Z" NotOnOrAfter="2011-07-24T10:36:41.125Z">
<saml2:AudienceRestriction>
<saml2:Audience>https://signin.aws.amazon.com/saml</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AttributeStatement>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">first.last@example.com</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">28800</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
<saml2:AuthnStatement AuthnInstant="2017-07-24T10:31:38.000Z" SessionIndex="_b1dd2c0469d905dfb1e10751d6feae95">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>
================================================
FILE: aws_google_auth/tests/saml-response-too-soon.xml
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://signin.aws.amazon.com/saml" ID="_7c434be06bf79a781dae9e7ed0024679" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_b1dd2c0469d905dfb1e10751d6feae95" IssueInstant="2017-07-24T10:31:41.125Z" Version="2.0">
<saml2:Issuer>https://accounts.google.com/o/saml2?idpid=abcd12345</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_b1dd2c0469d905dfb1e10751d6feae95">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>GbaJHVPpMT7JJEn+DtohU/tzd5b/BiZ9+It3sd2LB5Y=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>dJxZmFNw+rY07AV7Ex1Kbvn9ZiGE4VKwYELwxkrejgEiVeAteyaw8rQfeHDF1UhZJ/2JTHWs3uk+
VoWZcI1qcWO3HRjZ/jz7DXH/QGVIBYe447sr9o2RC2WfpjAYTDJ5rN5nPmrQKXxREfFzsZXJutcj
iPGXDNCC4SsWmKDaqbpWiDKhw+wRxtGxEXB2Ny11dRL6sCIHCdq86H55EXcq2YqL5I/ryMcWt3L0
SZ5B9aq80omhear/24M1HyL35dmxVUFODrYBxMQ+7Lw6/XUCA2k60MjcsHQW+BJZGwFJBL0HJywu
bc10BKTA89jbXyBtdoagtWRhF6LJzjL5bImLGA==</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509SubjectName>ST=California,C=US,OU=Google For Work,CN=Google,L=Mountain View,O=Google Inc.</ds:X509SubjectName>
<ds:X509Certificate>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</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">first.last@example.com</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotOnOrAfter="2017-07-24T10:36:41.125Z" Recipient="https://signin.aws.amazon.com/saml"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<!-- This will always be invalid for tests, hence the very far off NotBefore and NotOnOrAfter -->
<saml2:Conditions NotBefore="2100-07-24T10:26:41.125Z" NotOnOrAfter="2100-07-24T10:36:41.125Z">
<saml2:AudienceRestriction>
<saml2:Audience>https://signin.aws.amazon.com/saml</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AttributeStatement>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/RoleSessionName">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">first.last@example.com</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/Role">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/admin,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/read-only,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">arn:aws:iam::123456789012:role/test,arn:aws:iam::123456789012:saml-provider/GoogleApps</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="https://aws.amazon.com/SAML/Attributes/SessionDuration">
<saml2:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">28800</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
<saml2:AuthnStatement AuthnInstant="2017-07-24T10:31:38.000Z" SessionIndex="_b1dd2c0469d905dfb1e10751d6feae95">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
</saml2p:Response>
================================================
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__), "<class 'botocore.client.STS'>")
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 = "<xml></xml>"
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 = "<xml></xml>"
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_clien
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
SYMBOL INDEX (208 symbols across 16 files)
FILE: aws_google_auth/__init__.py
function parse_args (line 21) | def parse_args(args):
function exit_if_unsupported_python (line 58) | def exit_if_unsupported_python():
function cli (line 72) | def cli(cli_args):
function resolve_config (line 89) | def resolve_config(args):
function process_auth (line 196) | def process_auth(args, config):
function main (line 288) | def main():
FILE: aws_google_auth/amazon.py
class Amazon (line 17) | class Amazon:
method __init__ (line 19) | def __init__(self, config, saml_xml):
method sts_client (line 25) | def sts_client(self):
method base64_encoded_saml (line 38) | def base64_encoded_saml(self):
method token (line 42) | def token(self):
method access_key_id (line 51) | def access_key_id(self):
method secret_access_key (line 55) | def secret_access_key(self):
method session_token (line 59) | def session_token(self):
method expiration (line 63) | def expiration(self):
method print_export_line (line 66) | def print_export_line(self):
method roles (line 78) | def roles(self):
method assume_role (line 87) | def assume_role(self, role, principal, saml_assertion, duration=None, ...
method resolve_aws_aliases (line 121) | def resolve_aws_aliases(self, roles):
method is_valid_saml_assertion (line 160) | def is_valid_saml_assertion(saml_xml):
FILE: aws_google_auth/configuration.py
class Configuration (line 17) | class Configuration(object):
method __init__ (line 19) | def __init__(self, **kwargs):
method config_profile (line 48) | def config_profile(profile):
method max_duration (line 55) | def max_duration(self):
method credentials_file (line 59) | def credentials_file(self):
method config_file (line 63) | def config_file(self):
method saml_cache_file (line 67) | def saml_cache_file(self):
method ensure_config_files_exist (line 70) | def ensure_config_files_exist(self):
method saml_cache (line 82) | def saml_cache(self):
method saml_cache (line 89) | def saml_cache(self, value):
method raise_if_invalid (line 97) | def raise_if_invalid(self):
method write (line 152) | def write(self, amazon_object):
method read (line 216) | def read(self, profile):
FILE: aws_google_auth/google.py
class ExpectedGoogleException (line 32) | class ExpectedGoogleException(Exception):
method __init__ (line 33) | def __init__(self, *args):
class Google (line 37) | class Google:
method __init__ (line 38) | def __init__(self, config, save_failure, save_flow=False):
method login_url (line 63) | def login_url(self):
method check_for_failure (line 67) | def check_for_failure(self, sess):
method _save_file_name (line 98) | def _save_file_name(self, url):
method _save_request (line 104) | def _save_request(self, url, method='GET', data=None, json_data=None):
method _save_response (line 115) | def _save_response(self, url, response):
method post (line 121) | def post(self, url, data=None, json_data=None):
method get (line 141) | def get(self, url):
method parse_error_message (line 162) | def parse_error_message(sess):
method find_key_handles (line 172) | def find_key_handles(input, challengeTxt):
method find_app_id (line 198) | def find_app_id(inputString):
method do_login (line 207) | def do_login(self):
method check_extra_step (line 346) | def check_extra_step(response):
method parse_saml (line 352) | def parse_saml(self):
method handle_captcha (line 371) | def handle_captcha(self, sess, payload):
method handle_sk (line 450) | def handle_sk(self, sess):
method handle_sms (line 540) | def handle_sms(self, sess):
method handle_prompt (line 567) | def handle_prompt(self, sess):
method check_prompt_code (line 654) | def check_prompt_code(response):
method handle_totp (line 663) | def handle_totp(self, sess):
method handle_dp (line 694) | def handle_dp(self, sess):
method handle_iap (line 712) | def handle_iap(self, sess):
method handle_selectchallenge (line 830) | def handle_selectchallenge(self, sess):
FILE: aws_google_auth/tests/test_amazon.py
class TestAmazon (line 12) | class TestAmazon(unittest.TestCase):
method valid_config (line 15) | def valid_config(self):
method read_local_file (line 22) | def read_local_file(self, filename):
method test_sts_client (line 27) | def test_sts_client(self):
method test_role_extraction (line 31) | def test_role_extraction(self):
method test_role_extraction_too_many_commas (line 41) | def test_role_extraction_too_many_commas(self):
method test_invalid_saml_too_soon (line 52) | def test_invalid_saml_too_soon(self):
method test_invalid_saml_too_late (line 56) | def test_invalid_saml_too_late(self):
method test_invalid_saml_expired_before_valid (line 60) | def test_invalid_saml_expired_before_valid(self):
method test_invalid_saml_bad_input (line 64) | def test_invalid_saml_bad_input(self):
method test_valid_saml (line 71) | def test_valid_saml(self):
method test_sts_client_with_invalid_profile (line 76) | def test_sts_client_with_invalid_profile(self):
FILE: aws_google_auth/tests/test_args_parser.py
class TestPythonFailOnVersion (line 8) | class TestPythonFailOnVersion(unittest.TestCase):
method test_no_arguments (line 10) | def test_no_arguments(self):
method test_username (line 45) | def test_username(self):
method test_nocache (line 62) | def test_nocache(self):
method test_resolvealiases (line 79) | def test_resolvealiases(self):
method test_ask_and_supply_role (line 96) | def test_ask_and_supply_role(self):
method test_invalid_duration (line 101) | def test_invalid_duration(self):
FILE: aws_google_auth/tests/test_backwards_compatibility.py
class TestConfigurationPersistence (line 11) | class TestConfigurationPersistence(unittest.TestCase):
method setUp (line 13) | def setUp(self):
method tearDown (line 36) | def tearDown(self):
method test_configuration_backwards_compatibility (line 42) | def test_configuration_backwards_compatibility(self):
FILE: aws_google_auth/tests/test_config_parser.py
class TestProfileProcessing (line 10) | class TestProfileProcessing(unittest.TestCase):
method test_default (line 12) | def test_default(self):
method test_cli_param_supplied (line 17) | def test_cli_param_supplied(self):
method test_with_environment (line 23) | def test_with_environment(self):
class TestUsernameProcessing (line 33) | class TestUsernameProcessing(unittest.TestCase):
method test_default (line 35) | def test_default(self):
method test_cli_param_supplied (line 40) | def test_cli_param_supplied(self):
method test_with_environment (line 46) | def test_with_environment(self):
class TestDurationProcessing (line 56) | class TestDurationProcessing(unittest.TestCase):
method test_default (line 58) | def test_default(self):
method test_cli_param_supplied (line 63) | def test_cli_param_supplied(self):
method test_invalid_cli_param_supplied (line 68) | def test_invalid_cli_param_supplied(self):
method test_with_environment (line 75) | def test_with_environment(self):
class TestIDPProcessing (line 85) | class TestIDPProcessing(unittest.TestCase):
method test_default (line 87) | def test_default(self):
method test_cli_param_supplied (line 92) | def test_cli_param_supplied(self):
method test_with_environment (line 98) | def test_with_environment(self):
class TestSPProcessing (line 108) | class TestSPProcessing(unittest.TestCase):
method test_default (line 110) | def test_default(self):
method test_cli_param_supplied (line 115) | def test_cli_param_supplied(self):
method test_with_environment (line 121) | def test_with_environment(self):
class TestRegionProcessing (line 131) | class TestRegionProcessing(unittest.TestCase):
method test_default (line 134) | def test_default(self):
method test_cli_param_supplied (line 139) | def test_cli_param_supplied(self):
method test_with_environment (line 145) | def test_with_environment(self):
class TestRoleProcessing (line 155) | class TestRoleProcessing(unittest.TestCase):
method test_default (line 157) | def test_default(self):
method test_cli_param_supplied (line 162) | def test_cli_param_supplied(self):
method test_with_environment (line 168) | def test_with_environment(self):
class TestAskRoleProcessing (line 174) | class TestAskRoleProcessing(unittest.TestCase):
method test_default (line 176) | def test_default(self):
method test_cli_param_supplied (line 181) | def test_cli_param_supplied(self):
method test_with_environment (line 188) | def test_with_environment(self):
class TestU2FDisabledProcessing (line 194) | class TestU2FDisabledProcessing(unittest.TestCase):
method test_default (line 196) | def test_default(self):
method test_cli_param_supplied (line 201) | def test_cli_param_supplied(self):
method test_with_environment (line 208) | def test_with_environment(self):
class TestResolveAliasesProcessing (line 214) | class TestResolveAliasesProcessing(unittest.TestCase):
method test_default (line 216) | def test_default(self):
method test_cli_param_supplied (line 221) | def test_cli_param_supplied(self):
method test_with_environment (line 228) | def test_with_environment(self):
class TestBgResponseProcessing (line 234) | class TestBgResponseProcessing(unittest.TestCase):
method test_default (line 236) | def test_default(self):
method test_cli_param_supplied (line 241) | def test_cli_param_supplied(self):
method test_with_environment (line 248) | def test_with_environment(self):
class TestAccountProcessing (line 254) | class TestAccountProcessing(unittest.TestCase):
method test_default (line 257) | def test_default(self):
method test_cli_param_supplied (line 262) | def test_cli_param_supplied(self):
method test_with_environment (line 268) | def test_with_environment(self):
FILE: aws_google_auth/tests/test_configuration.py
class TestConfigurationMethods (line 8) | class TestConfigurationMethods(unittest.TestCase):
method test_config_profile (line 10) | def test_config_profile(self):
method test_duration_invalid_values (line 17) | def test_duration_invalid_values(self):
method test_duration_valid_values (line 61) | def test_duration_valid_values(self):
method test_duration_defaults_to_max_duration (line 78) | def test_duration_defaults_to_max_duration(self):
method test_ask_role_invalid_values (line 88) | def test_ask_role_invalid_values(self):
method test_ask_role_valid_values (line 101) | def test_ask_role_valid_values(self):
method test_ask_role_optional (line 121) | def test_ask_role_optional(self):
method test_idp_id_invalid_values (line 131) | def test_idp_id_invalid_values(self):
method test_idp_id_valid_values (line 142) | def test_idp_id_valid_values(self):
method test_sp_id_invalid_values (line 155) | def test_sp_id_invalid_values(self):
method test_username_valid_values (line 166) | def test_username_valid_values(self):
method test_username_invalid_values (line 179) | def test_username_invalid_values(self):
method test_password_valid_values (line 200) | def test_password_valid_values(self):
method test_password_invalid_values (line 213) | def test_password_invalid_values(self):
method test_sp_id_valid_values (line 234) | def test_sp_id_valid_values(self):
method test_profile_defaults_to_sts (line 247) | def test_profile_defaults_to_sts(self):
method test_profile_invalid_values (line 257) | def test_profile_invalid_values(self):
method test_profile_valid_values (line 270) | def test_profile_valid_values(self):
method test_profile_defaults (line 284) | def test_profile_defaults(self):
method test_region_invalid_values (line 294) | def test_region_invalid_values(self):
method test_region_valid_values (line 306) | def test_region_valid_values(self):
method test_region_defaults_to_none (line 319) | def test_region_defaults_to_none(self):
method test_role_arn_invalid_values (line 330) | def test_role_arn_invalid_values(self):
method test_role_arn_is_optional (line 355) | def test_role_arn_is_optional(self):
method test_role_arn_valid_values (line 365) | def test_role_arn_valid_values(self):
method test_u2f_disabled_invalid_values (line 379) | def test_u2f_disabled_invalid_values(self):
method test_u2f_disabled_valid_values (line 392) | def test_u2f_disabled_valid_values(self):
method test_u2f_disabled_is_optional (line 412) | def test_u2f_disabled_is_optional(self):
method test_unicode_password (line 422) | def test_unicode_password(self):
FILE: aws_google_auth/tests/test_configuration_persistence.py
class TestConfigurationPersistence (line 11) | class TestConfigurationPersistence(unittest.TestCase):
method setUp (line 13) | def setUp(self):
method tearDown (line 41) | def tearDown(self):
method test_creating_new_profile (line 47) | def test_creating_new_profile(self):
method test_password_not_written (line 61) | def test_password_not_written(self):
method test_can_read_all_values (line 71) | def test_can_read_all_values(self):
FILE: aws_google_auth/tests/test_google.py
class TestGoogle (line 15) | class TestGoogle(unittest.TestCase):
method read_local_file (line 16) | def read_local_file(self, filename):
method test_extra_step (line 21) | def test_extra_step(self):
method test_find_keyhandles (line 27) | def test_find_keyhandles(self):
method test_parse_saml_without_login (line 51) | def test_parse_saml_without_login(self):
method test_parse_saml_without_save (line 61) | def test_parse_saml_without_save(self):
method test_parse_saml_with_save (line 84) | def test_parse_saml_with_save(self):
FILE: aws_google_auth/tests/test_init.py
class TestInit (line 9) | class TestInit(unittest.TestCase):
method setUp (line 11) | def setUp(self):
method test_main_method_has_no_parameters (line 15) | def test_main_method_has_no_parameters(self, mock_cli):
method test_main_method_chaining (line 31) | def test_main_method_chaining(self, process_auth, resolve_config, exit...
method test_process_auth_standard (line 99) | def test_process_auth_standard(self, mock_google, mock_amazon, mock_ut...
method test_process_auth_print_creds (line 174) | def test_process_auth_print_creds(self, mock_google, mock_amazon, mock...
method test_process_auth_specified_role (line 253) | def test_process_auth_specified_role(self, mock_google, mock_amazon, m...
method test_process_auth_dont_resolve_alias (line 322) | def test_process_auth_dont_resolve_alias(self, mock_google, mock_amazo...
method test_process_auth_with_profile (line 394) | def test_process_auth_with_profile(self, mock_google, mock_amazon, moc...
method test_process_auth_with_saml_cache (line 468) | def test_process_auth_with_saml_cache(self, mock_google, mock_amazon, ...
FILE: aws_google_auth/tests/test_python_version.py
class TestPythonFailOnVersion (line 8) | class TestPythonFailOnVersion(unittest.TestCase):
method test_python26 (line 10) | def test_python26(self):
method test_python27 (line 21) | def test_python27(self):
method test_python30 (line 31) | def test_python30(self):
FILE: aws_google_auth/tests/test_util.py
class TestUtilMethods (line 11) | class TestUtilMethods(unittest.TestCase):
method test_coalesce_no_arguments (line 13) | def test_coalesce_no_arguments(self):
method test_coalesce_one_argument (line 16) | def test_coalesce_one_argument(self):
method test_coalesce_two_arguments (line 21) | def test_coalesce_two_arguments(self):
method test_coalesce_many_arguments (line 28) | def test_coalesce_many_arguments(self):
method test_unicode_to_string_if_needed_python_3 (line 33) | def test_unicode_to_string_if_needed_python_3(self):
method test_unicode_to_string_if_needed_python_2 (line 39) | def test_unicode_to_string_if_needed_python_2(self):
method test_unicode_to_string_if_needed (line 48) | def test_unicode_to_string_if_needed(self):
method test_get_password_when_tty (line 55) | def test_get_password_when_tty(self, mock_stdin, mock_getpass):
method test_get_password_when_not_tty (line 63) | def test_get_password_when_not_tty(self, mock_stdin):
FILE: aws_google_auth/u2f.py
function __appid_verifier__fetch_json (line 20) | def __appid_verifier__fetch_json(app_id):
function __appid_verifier__valid_facets (line 41) | def __appid_verifier__valid_facets(app_id, facets):
function u2f_auth (line 45) | def u2f_auth(challenges, facet):
FILE: aws_google_auth/util.py
class Util (line 14) | class Util:
method get_input (line 17) | def get_input(prompt):
method pick_a_role (line 21) | def pick_a_role(roles, aliases=None, account=None):
method touch (line 68) | def touch(file_name, mode=0o600):
method coalesce (line 80) | def coalesce(*args):
method unicode_to_string_if_needed (line 87) | def unicode_to_string_if_needed(object):
method get_password (line 94) | def get_password(prompt):
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (244K chars).
[
{
"path": ".github/workflows/pythonpackage.yml",
"chars": 1489,
"preview": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more inform"
},
{
"path": ".github/workflows/pythonrelease.yml",
"chars": 864,
"preview": "# This workflow will upload a Python Package using Twine when a release is created\n# For more information see: https://h"
},
{
"path": ".github/workflows/rstlint.yml",
"chars": 496,
"preview": "\nname: Lint RST\n\non:\n push:\n branches: [ master ]\n pull_request:\n branches: [ master ]\n\njobs:\n build:\n runs-"
},
{
"path": ".gitignore",
"chars": 76,
"preview": "*.swp\n*.egg-info\n.eggs/\n*.pyc\ndist\nbuild/\n.idea/\nPipfile\nPipfile.lock\nvenv/*"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3219,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "CONTRIBUTING.md",
"chars": 1043,
"preview": "# Contributing\n\nContributions are welcome! The most valuable contributions, in order of preference, are:\n\n1. Pull reques"
},
{
"path": "Dockerfile",
"chars": 644,
"preview": "FROM alpine:3.5\n\nRUN apk add --update-cache py3-pip ca-certificates py3-certifi py3-lxml\\\n pyt"
},
{
"path": "Dockerfile.python2",
"chars": 518,
"preview": "FROM alpine:3.5\n\nRUN apk add --update-cache py2-pip ca-certificates py2-certifi py2-lxml \\\n py"
},
{
"path": "LICENSE.txt",
"chars": 1080,
"preview": "MIT License\n\nCopyright (c) 2016 Cevo Australia, Pty Ltd\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.rst",
"chars": 12571,
"preview": "aws-google-auth\n===============\n\n|github-badge| |docker-badge| |pypi-badge| |coveralls-badge|\n\n.. |github-badge| image::"
},
{
"path": "aws_google_auth/__init__.py",
"chars": 11609,
"preview": "#!/usr/bin/env python\nfrom __future__ import print_function\n\nimport argparse\nimport base64\nimport os\nimport sys\nimport l"
},
{
"path": "aws_google_auth/_version.py",
"chars": 23,
"preview": "__version__ = \"0.0.38\"\n"
},
{
"path": "aws_google_auth/amazon.py",
"chars": 6704,
"preview": "#!/usr/bin/env python\n\nimport base64\nimport boto3\nimport os\nimport re\n\nfrom datetime import datetime\nfrom threading impo"
},
{
"path": "aws_google_auth/configuration.py",
"chars": 12531,
"preview": "#!/usr/bin/env python\n\nimport os\n\nimport botocore.session\nimport filelock\n\ntry:\n from backports import configparser\ne"
},
{
"path": "aws_google_auth/google.py",
"chars": 33179,
"preview": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\nfrom __future__ import print_function\n\nimport base64\nimport io\nimport json"
},
{
"path": "aws_google_auth/tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "aws_google_auth/tests/google_error.html",
"chars": 28026,
"preview": "<!DOCTYPE doctype html>\n<html dir=\"ltr\" lang=\"en-GB\"><head><base href=\"https://accounts.google.com/\"/><script data-id=\"_"
},
{
"path": "aws_google_auth/tests/saml-response-expired-before-valid.xml",
"chars": 6086,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<saml2p:Response xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protoc"
},
{
"path": "aws_google_auth/tests/saml-response-no-expire.xml",
"chars": 6080,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<saml2p:Response xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protoc"
},
{
"path": "aws_google_auth/tests/saml-response-too-late.xml",
"chars": 6096,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<saml2p:Response xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protoc"
},
{
"path": "aws_google_auth/tests/saml-response-too-soon.xml",
"chars": 6096,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<saml2p:Response xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protoc"
},
{
"path": "aws_google_auth/tests/test_amazon.py",
"chars": 3417,
"preview": "#!/usr/bin/env python\n\nimport unittest\nimport mock\n\nfrom aws_google_auth import amazon\nfrom aws_google_auth import confi"
},
{
"path": "aws_google_auth/tests/test_args_parser.py",
"chars": 3920,
"preview": "#!/usr/bin/env python\n\nimport unittest\n\nfrom aws_google_auth import parse_args\n\n\nclass TestPythonFailOnVersion(unittest."
},
{
"path": "aws_google_auth/tests/test_backwards_compatibility.py",
"chars": 2592,
"preview": "#!/usr/bin/env python\n\nimport unittest\nfrom random import randint\n\nimport configparser\n\nfrom aws_google_auth import conf"
},
{
"path": "aws_google_auth/tests/test_config_parser.py",
"chars": 8488,
"preview": "import os\nimport unittest\n\nimport mock\nfrom nose.tools import nottest\n\nfrom aws_google_auth import resolve_config, parse"
},
{
"path": "aws_google_auth/tests/test_configuration.py",
"chars": 15589,
"preview": "#!/usr/bin/env python\n\nimport unittest\n\nfrom aws_google_auth import configuration\n\n\nclass TestConfigurationMethods(unitt"
},
{
"path": "aws_google_auth/tests/test_configuration_persistence.py",
"chars": 4522,
"preview": "#!/usr/bin/env python\n\nimport unittest\nfrom random import randint\n\nimport configparser\n\nfrom aws_google_auth import conf"
},
{
"path": "aws_google_auth/tests/test_google.py",
"chars": 3527,
"preview": "# -*- coding: utf-8 -*-\nimport unittest\nfrom io import open\nfrom os import path\n\nimport json\nimport base64\n\nfrom bs4 imp"
},
{
"path": "aws_google_auth/tests/test_init.py",
"chars": 24452,
"preview": "import unittest\nfrom argparse import Namespace\n\nfrom mock import call, patch, Mock, MagicMock\n\nimport aws_google_auth\n\n\n"
},
{
"path": "aws_google_auth/tests/test_python_version.py",
"chars": 1121,
"preview": "from aws_google_auth import exit_if_unsupported_python\n\nimport unittest\nimport sys\nimport mock\n\n\nclass TestPythonFailOnV"
},
{
"path": "aws_google_auth/tests/test_util.py",
"chars": 2861,
"preview": "#!/usr/bin/env python\n\nimport sys\nimport unittest\n\nfrom mock import patch, MagicMock\n\nfrom aws_google_auth import util\n\n"
},
{
"path": "aws_google_auth/tests/too-many-commas.xml",
"chars": 5997,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<saml2p:Response xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protoc"
},
{
"path": "aws_google_auth/tests/valid-response.xml",
"chars": 5994,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<saml2p:Response xmlns:saml2p=\"urn:oasis:names:tc:SAML:2.0:protoc"
},
{
"path": "aws_google_auth/u2f.py",
"chars": 3187,
"preview": "#!/usr/bin/env python\n\nimport json\nimport time\n\nimport requests\nfrom u2flib_host import u2f, exc, appid\nfrom u2flib_host"
},
{
"path": "aws_google_auth/util.py",
"chars": 3370,
"preview": "#!/usr/bin/env python\n\nfrom __future__ import print_function\n\nimport getpass\nimport os\nimport sys\nfrom collections impor"
},
{
"path": "requirements.txt",
"chars": 93,
"preview": "beautifulsoup4\nboto3\nconfigparser\nfilelock\nkeyring\nlxml\nPillow\nrequests\nsix\ntabulate\ntzlocal\n"
},
{
"path": "setup.py",
"chars": 4662,
"preview": "\"\"\"A setuptools based setup module.\n\nSee:\nhttps://packaging.python.org/en/latest/distributing.html\nhttps://github.com/py"
}
]
About this extraction
This page contains the full source code of the cevoaustralia/aws-google-auth GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (226.8 KB), approximately 63.3k tokens, and a symbol index with 208 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.