Skip to content

Commit

Permalink
Allow to login to google using the browser
Browse files Browse the repository at this point in the history
  • Loading branch information
mcfedr committed Mar 3, 2021
1 parent 19a48a5 commit 61ab712
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 16 deletions.
46 changes: 31 additions & 15 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)
--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)
-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.
--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.
--port PORT Port for the redirect server ($PORT)
-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)
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
24 changes: 23 additions & 1 deletion aws_google_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ 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_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)')
Expand All @@ -42,6 +45,7 @@ def parse_args(args):
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.')
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')
Expand Down Expand Up @@ -160,6 +164,11 @@ def resolve_config(args):
os.getenv('RESOLVE_AWS_ALIASES'),
config.resolve_aliases)

config.browser = coalesce(
args.browser,
os.getenv('GOOGLE_BROWSER'),
config.browser)

# Username (Option priority = ARGS, ENV_VAR, DEFAULT)
config.username = coalesce(
args.username,
Expand Down Expand Up @@ -190,13 +199,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 +230,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

0 comments on commit 61ab712

Please sign in to comment.