Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to login to google using the browser #225

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,21 @@ You can find the ``GOOGLE_IDP_ID``, again from the admin console, via
string like ``https://accounts.google.com/o/saml2/idp?idpid=aBcD01AbC``
where the last bit (after the ``=``) is the IDP ID.

Browser Login
~~~~~~~~~~~~~~

To enable browser based login, you will need to host the redirect server
somewhere, for example

.. code:: shell

gcloud run deploy --image cevoaustralia/aws-google-auth --args=--redirect-server --platform managed

Then change your SAML settings so the ``ACS URL`` points to the redirect server.

You will also need to change the Trust Relationship of your IAM Role to allow ``SAML:aud``
to be the host of your redirect server.

Installation
------------

Expand Down Expand Up @@ -114,47 +129,48 @@ 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]
usage: aws-google-auth [-h] [-u USERNAME | -b | --redirect-server] [-I IDP_ID] [-S SP_ID] [-R REGION]
[-d DURATION | --auto-duration] [-p PROFILE] [-A ACCOUNT] [-D] [-q] [--bg-response BG_RESPONSE]
[--saml-assertion SAML_ASSERTION] [--no-cache] [--print-creds] [--resolve-aliases]
[--save-failure-html] [--save-saml-flow] [--port PORT] [-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)
-b, --browser Google login in the browser (Requires SAML redirect server) ($GOOGLE_BROWSER=1)
--redirect-server Run the redirect server on port ($PORT)
-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)
Credential duration in seconds (defaults to value of $DURATION, then falls back to 43200)
--auto-duration Tries to use the longest allowed duration ($AUTO_DURATION=1)
-p PROFILE, --profile PROFILE
AWS profile (defaults to value of $AWS_PROFILE, then
falls back to 'sts')
AWS profile (defaults to value of $AWS_PROFILE, then falls back to 'sts')
-A ACCOUNT, --account ACCOUNT
Filter for specific AWS account.
-D, --disable-u2f Disable U2F functionality.
-q, --quiet Quiet output
--bg-response BG_RESPONSE
Override default bgresponse challenge token ($GOOGLE_BG_RESPONSE).
Override default bgresponse challenge token.
--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.
--resolve-aliases Resolve AWS account aliases. ($RESOLVE_AWS_ALIASES=1)
--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
--port PORT Port for the redirect server ($PORT)
-a, --ask-role Set true to always pick the role ($AWS_ASK_ROLE=1)
-r ROLE_ARN, --role-arn ROLE_ARN
The ARN of the role to assume ($AWS_ROLE_ARN)
The ARN of the role to assume
-k, --keyring Use keyring for storing the password.
-l {debug,info,warn}, --log {debug,info,warn}
Select log level (default: warn)
Expand Down
48 changes: 27 additions & 21 deletions aws_google_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ def parse_args(args):
description="Acquire temporary AWS credentials via Google SSO",
)

parser.add_argument('-u', '--username', help='Google Apps username ($GOOGLE_USERNAME)')
google_group = parser.add_mutually_exclusive_group()
google_group.add_argument('-u', '--username', help='Google Apps username ($GOOGLE_USERNAME)')
google_group.add_argument('-b', '--browser', action='store_true', help='Google login in the browser (Requires SAML redirect server) ($GOOGLE_BROWSER=1)')
google_group.add_argument('--redirect-server', action='store_true', help='Run the redirect server on port ($PORT)')
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)')
duration_group.add_argument('--auto-duration', action='store_true', help='Tries to use the longest allowed duration ($AUTO_DURATION=1)')
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.')
Expand All @@ -39,12 +42,13 @@ def parse_args(args):
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('--resolve-aliases', action='store_true', help='Resolve AWS account aliases. ($RESOLVE_AWS_ALIASES=1)')
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.')
parser.add_argument('--port', type=int, help='Port for the redirect server ($PORT)')

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('-a', '--ask-role', action='store_true', help='Set true to always pick the role ($AWS_ASK_ROLE=1)')
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',
Expand Down Expand Up @@ -106,10 +110,7 @@ def resolve_config(args):
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))
config.ask_role = args.ask_role or os.getenv('AWS_ASK_ROLE') != None

# Duration (Option priority = ARGS, ENV_VAR, DEFAULT)
config.duration = int(coalesce(
Expand All @@ -118,11 +119,7 @@ def resolve_config(args):
config.duration))

# Automatic duration (Option priority = ARGS, ENV_VAR, DEFAULT)
config.auto_duration = coalesce(
args.auto_duration,
os.getenv('AUTO_DURATION'),
config.auto_duration
)
config.auto_duration = args.auto_duration or os.getenv('AUTO_DURATION') != None

# IDP ID (Option priority = ARGS, ENV_VAR, DEFAULT)
config.idp_id = coalesce(
Expand All @@ -149,16 +146,12 @@ def resolve_config(args):
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)
config.u2f_disabled = args.disable_u2f or os.getenv('U2F_DISABLED') != None

# 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)
config.resolve_aliases = args.resolve_aliases or os.getenv('RESOLVE_AWS_ALIASES') != None

config.browser = args.browser or os.getenv('GOOGLE_BROWSER') != None

# Username (Option priority = ARGS, ENV_VAR, DEFAULT)
config.username = coalesce(
Expand Down Expand Up @@ -190,13 +183,23 @@ def resolve_config(args):
os.getenv('GOOGLE_BG_RESPONSE'),
config.bg_response)

config.port = int(coalesce(
args.port,
os.getenv('PORT'),
config.port))

return config


def process_auth(args, config):
# Set up logging
logging.getLogger().setLevel(getattr(logging, args.log_level.upper(), None))

if args.redirect_server:
from aws_google_auth.redirect_server import start_redirect_server
start_redirect_server(config.port)
return

if config.region is None:
config.region = util.Util.get_input("AWS Region: ")
logging.debug('%s: region is: %s', __name__, config.region)
Expand All @@ -211,6 +214,9 @@ def process_auth(args, config):
elif args.saml_cache and config.saml_cache:
saml_xml = config.saml_cache
logging.info('%s: SAML cache found', __name__)
elif config.browser:
google_client = google.Google(config, save_failure=args.save_failure_html, save_flow=args.save_saml_flow)
saml_xml = google_client.do_browser_saml()
else:
# No cache, continue without.
logging.info('%s: SAML cache not found', __name__)
Expand Down
10 changes: 10 additions & 0 deletions aws_google_auth/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def __init__(self, **kwargs):
self.quiet = False
self.bg_response = None
self.account = ""
self.browser = False
self.port = 8000

# For the "~/.aws/config" file, we use the format "[profile testing]"
# for the 'testing' profile. The credential file will just be "[testing]"
Expand Down Expand Up @@ -146,6 +148,9 @@ def raise_if_invalid(self):
# account
assert (self.account.__class__ is str), "Expected account to be string. Got {}".format(self.account.__class__)

# port
assert (self.port.__class__ is int), "Expected port to be an integer. Got {}.".format(self.port.__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.
Expand Down Expand Up @@ -173,6 +178,7 @@ def write(self, amazon_object):
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)
config_parser.set(profile, 'google_config.browser', self.browser)

with open(self.config_file, 'w+') as f:
config_parser.write(f)
Expand Down Expand Up @@ -271,6 +277,10 @@ def read(self, profile):
read_account = unicode_to_string(config_parser[profile_string].get('account', None))
self.account = coalesce(read_account, self.account)

# Browser
read_browser = config_parser[profile_string].getboolean('google_config.browser', None)
self.browser = coalesce(read_browser, self.browser)

# SAML Cache
try:
with open(self.saml_cache_file, 'r') as f:
Expand Down
19 changes: 19 additions & 0 deletions aws_google_auth/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os
import re
import sys
import webbrowser

import requests
from PIL import Image
Expand All @@ -20,6 +21,7 @@
from six.moves import urllib_parse, input

from aws_google_auth import _version
from aws_google_auth.login_server import LoginServerHandler, LoginServer

# The U2F USB Library is optional, if it's there, include it.
try:
Expand Down Expand Up @@ -59,6 +61,23 @@ def __init__(self, config, save_failure, save_flow=False):
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)

def do_browser_saml(self):
logging.info('Opening url %s', self.login_url)
webbrowser.open(self.login_url)
saml_text = self._catch_saml()

return base64.b64decode(saml_text)

@staticmethod
def _catch_saml(port=4589):
server_address = ('', port)
httpd = LoginServer(server_address, LoginServerHandler)
logging.info('Starting http handler...\n')
httpd.handle_request()

assert ("SAMLResponse" in httpd.post_data), "Expected post data to contain SAMLResponse."
return httpd.post_data["SAMLResponse"][0]

@property
def login_url(self):
return self.base_url + "/o/saml2/initsso?idpid={}&spid={}&forceauthn=false".format(
Expand Down
35 changes: 35 additions & 0 deletions aws_google_auth/login_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env python
"""
This HTTP server for capturing the SAMLResponse that is redirected to 127.0.0.1
"""

from http.server import BaseHTTPRequestHandler, HTTPServer
import logging

from aws_google_auth import util

class LoginServer(HTTPServer):
post_data = {}


class LoginServerHandler(BaseHTTPRequestHandler):
def _set_response(self):
self.send_response(200)
self.send_header('content-type', 'text/html')
self.end_headers()
self.wfile.write("""
<html>
<head><title>Success</title></head>
<body>
Check your console
<script>window.close()</script>
</body>
</html>
""".encode("utf-8"))

def do_POST(self):
self.server.post_data = util.Util.parse_post(self)
logging.debug("POST request,\nPath: %s\nHeaders:\n%s\n\nBody:\n%s\n",
str(self.path), str(self.headers), self.server.post_data)

self._set_response()
25 changes: 25 additions & 0 deletions aws_google_auth/redirect_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python
"""
This HTTP server can be run on a server, and redirects the SAMLResponse to 127.0.0.1 so the command can capture it
"""

from http.server import BaseHTTPRequestHandler, HTTPServer
import logging

class RedirectServerHandler(BaseHTTPRequestHandler):
def do_POST(self):
self.send_response(307)
self.send_header('location', 'http://127.0.0.1:4589/')
self.end_headers()

def start_redirect_server(port):
logging.basicConfig(level=logging.INFO)
server_address = ('', port)
httpd = HTTPServer(server_address, RedirectServerHandler)
logging.info('Starting http redirect server on: %s', port)
try:
httpd.serve_forever()
except KeyboardInterrupt:
pass
httpd.server_close()
logging.info('Stopping http redirect server')
16 changes: 16 additions & 0 deletions aws_google_auth/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import os
import sys
from collections import OrderedDict
from urllib.parse import parse_qs
from cgi import parse_header, parse_multipart

from six.moves import input
from tabulate import tabulate
Expand Down Expand Up @@ -100,3 +102,17 @@ def get_password(prompt):
password = sys.stdin.readline()
print("")
return password

@staticmethod
def parse_post(handler):
if 'content-type' not in handler.headers:
return {}
ctype, pdict = parse_header(handler.headers['content-type'])
if ctype == 'multipart/form-data':
postvars = parse_multipart(handler.rfile, pdict)
elif ctype == 'application/x-www-form-urlencoded':
length = int(handler.headers['content-length'])
postvars = parse_qs(handler.rfile.read(length).decode('utf-8'), keep_blank_values=1)
else:
postvars = {}
return postvars