Skip to content

Commit

Permalink
Fix OTP not working and add content to README
Browse files Browse the repository at this point in the history
  • Loading branch information
trewq34 committed Jun 5, 2022
1 parent dd06fd9 commit d21e548
Show file tree
Hide file tree
Showing 7 changed files with 165 additions and 52 deletions.
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
venv
.git
*.egg-info/
*.egg-info
.DS_Store
dist
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,5 @@ dmypy.json

# Pyre type checker
.pyre/

.DS_Store
123 changes: 121 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,121 @@
# auther
Command line tool for AWS CLI authentication
# `auther`

Auther is a CLI tool which authenticates your AWS CLI using various identity providers - all in one tool!

Currently supported Identity Providers

- Azure Active Directory - `azuread`

---

## Installation

### Pip

```console
$ pip install auther
```

Further info on [PyPi](https://pypi.org/project/auther/).

### Docker

```console
$ docker run -it --rm -v ~/.aws:/root/.aws trewq34/auther
```

When looking at usage information, the above command is a direct replacement for `auther`. Although you may wish to create a command alias with your chosen shell for ease.

Further info on [Docker Hub](https://hub.docker.com/r/trewq34/auther).

## Usage

### Configure

Before using `auther` to authenticate your AWS CLI, you need to configure it. This can be done quite using `auther configure`

```console
# Uses the default options, most importantly: AWS config file path, AWS region, AWS profile and Auther provider
$ auther configure
Your Azure AD Tenant ID: 30e04ef1-fb0d-4844-87a5-8720745de01b
Your Azure AD Application ID: 94ab3a5d-1b99-416a-bcaf-669f7b6bcaba
The username you use to sign in: [email protected]
```

If you need to use a different AWS CLI profile or AWS region, you can override these by passing in options to the `configure` command

```console
$ auther configure --profile saml --region us-east-1
```

This will create/update a CLI profile called `saml` for use in the `us-east-1` region.

For all available configuration options and their defaults, you can use the following command

```console
$ auther configure --help
```

### Login

Once you have configured your AWS CLI profile for use with `auther` for authentication, you can login simply using the following command

```console
# Uses the default options, most importantly: AWS config file path, AWS credential file path, AWS profile and Auther provider
$ auther login
```

If you wish to override override any of the defaults, you can do so by passing in options to the `login` command. A list of available options and their defailts is available using the following command

```console
$ auther login --help
```

## Troubleshooting

### Chromium failed to download

A common cause for this is a corporate proxy/firewall blocking such downloads. To work around this, you can set the `CHROME_BIN` environment variable pointing to your preinstalled Google Chrome, Chromium or Microsoft Edge installation (this will probably work with other Chromium based browsers too, although hasn't been tested).

Some examples per OS can be seen below

#### macOS

```console
# Google Chrome
$ export CHROME_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"

# Microsoft Edge
$ export CHROME_BIN="/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"

# Chromium
$ export CHROME_BIN="/Applications/Chromium.app/Contents/MacOS/Chromium"
```

#### Windows

```powershell
# Google Chrome
PS C:\Users\username> $env:CHROME_BIN="C:\Program Files\Google\Chrome\Application\chrome.exe"
# Microsoft Edge
PS C:\Users\username> $env:CHROME_BIN="C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
# Chromium - this depends on how you installed it. Assuming you installed it the same way I did, the path will be
PS C:\Users\username> $env:CHROME_BIN="C:\Users\username\AppData\Local\Chromium\Application\chrome.exe"
```

#### Linux

This will vary vastly depending on which distro you use. I've tested on RHEL 7.9, so this may not be the same as your distro. In any case, you can verify the path using the `which` command.

```console
# Google Chrome
$ export CHROME_BIN="/usr/bin/google-chrome"

# Microsoft Edge
$ export CHROME_BIN="/usr/bin/microsoft-edge"

# Chromium
$ export CHROME_BIN="/usr/bin/chromium-browser"
```
73 changes: 33 additions & 40 deletions auther/cli.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import click
import typer

from auther.exceptions import *
from auther.providers import *

from pathlib import Path

@click.group()
@click.version_option()
def main():
pass

@main.command('configure', help='Configure a chosen identiy provider for use')
@click.option('--aws-config', default=f'{Path.home()}/.aws/config', help='The path to your AWS config file', required=False)
@click.option('--profile', default='default', help='The name of the AWS profile', required=False)
@click.option('--region', default='eu-west-1', help='Your prefered AWS region', required=False)
@click.option('--output', default='json', help='Your prefered output format', required=False)
@click.option('--provider', default='azuread', help='The federated provider', required=False)
def configure(**kwargs):
provider = kwargs['provider']
app = typer.Typer(no_args_is_help=True)

@app.command(help='Configure a chosen identiy provider for use')
def configure(
aws_config: str = typer.Option(f'{Path.home()}/.aws/config', help='The path to your AWS config file'),
profile: str = typer.Option('default', help='The name of the AWS profile'),
region: str = typer.Option('eu-west-1', help='Your prefered AWS region'),
output: str = typer.Option('json', help='Your prefered AWS CLI output format'),
provider: str = typer.Option('azuread', help='The federated provider')
):
try:
provider_options = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').provider_options()
prefix = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').prefix()
Expand All @@ -39,40 +35,39 @@ def configure(**kwargs):
del opt['required']

if option != 'password':
options[prefix + option] = click.prompt(text, **opt)

options['output'] = kwargs['output']
options['region'] = kwargs['region']
options[prefix + option] = typer.prompt(text, **opt)

getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').write_config(options, kwargs['profile'], kwargs['aws_config'])
options['output'] = output
options['region'] = region

@main.command('login', help='Authenticate using a specified identity provider')
@click.option('--provider', default='azuread', help='The federated provider', required=False)
@click.option('--profile', default='default', help='The name of the AWS profile', required=False)
@click.option('--aws-config', default=f'{Path.home()}/.aws/config', help='The path to your AWS config file', required=False)
@click.option('--aws-creds', default=f'{Path.home()}/.aws/credentials', help='The path to your AWS credentials file', required=False)
def login(**kwargs):
provider = kwargs['provider']
getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').write_config(options, profile, aws_config)

@app.command(help='Authenticate using a specified identity provider')
def login(
provider: str = typer.Option('azuread', help='The federated provider'),
profile: str = typer.Option('default', help='The name of the AWS profile'),
aws_config: str = typer.Option(f'{Path.home()}/.aws/config', help='The path to your AWS config file'),
aws_creds: str = typer.Option(f'{Path.home()}/.aws/credentials', help='The path to your AWS credentials file'),
):
try:
opt_list = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').list_options()
provider_options = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').provider_options()
config = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').get_config(kwargs['aws_config'], kwargs['profile'], provider, opt_list)
config = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider').get_config(aws_config, profile, provider, opt_list)
except AttributeError:
raise ProviderNotFound(f"The provider {provider} doesn't have the correct module structure.")
except (NameError, KeyError):
raise ProviderNotFound(f"The provider {provider} doesn't exist")

opts = {
'profile': kwargs['profile'],
'aws_config': kwargs['aws_config'],
'aws_creds': kwargs['aws_creds']
'profile': profile,
'aws_config': aws_config,
'aws_creds': aws_creds
}

for option in provider_options:
opts[option.get('function')] = config.get(f"{provider}_{option.get('function')}")

opts['password'] = click.prompt(f'Enter the password for {opts.get("username")}', type=str, hide_input=True)
opts['password'] = typer.prompt(f'Enter the password for {opts.get("username")}', type=str, hide_input=True)

auth_provider = getattr(globals()[provider], f'{provider.replace("_", "").title()}Provider')(opts)

Expand All @@ -85,18 +80,16 @@ def login(**kwargs):
raise ProviderAuthenticationError(f'Provider {provider} returned no available roles')

if len(roles) > 1:
_output_roles(roles)
chosen_role = click.prompt(f'Enter the index of your chosen role', type=int)
for index, role in enumerate(roles):
print(f'[{index}] - {role[1]}')
chosen_role = typer.prompt('Enter the index of your chosen role', type=int)
else:
chosen_role = 0

duration = click.prompt(f'Enter session duration in hours', type=int, default=1)
duration = typer.prompt('Enter session duration in hours', type=int, default=1)

auth_provider.assume_role(provider, kwargs['profile'], roles[chosen_role], duration * 60 * 60)

def _output_roles(roles):
for index, role in enumerate(roles):
print(f'[{index}] - {role[1]}')
auth_provider.assume_role(provider, profile, roles[chosen_role], duration * 60 * 60)


if __name__ == '__main__':
main()
app()
7 changes: 2 additions & 5 deletions auther/providers/helpers/azuread.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,7 @@ async def _input_password(page, password):
await asyncio.sleep(0.25)

async def _input_code(page, code):
input_selector = 'input[type="tel"][name="otc"]'
checkbox_selector = 'input[type="checkbox"][name="rememberMFA"]'
input_selector = 'input[name="otc"]'
submit_selector = 'input[type="submit"][value="Verify"]'
error_selector = "#idSpan_SAOTCC_Error_OTC"

Expand All @@ -140,7 +139,6 @@ async def _input_code(page, code):
code = input("One-time code: ")

await page.type(input_selector, code)
await page.click(checkbox_selector)
await page.click(submit_selector)

while True:
Expand All @@ -151,7 +149,6 @@ async def _input_code(page, code):
await page.evaluate(
f"() => document.querySelector('{input_selector}').value = ''"
)
await page.click(checkbox_selector)
print("Incorrect code, try again")
break
# wait for one of the above to appear
Expand Down Expand Up @@ -214,7 +211,7 @@ async def _auth(url, username=None, password=None, headless=True, stay_signed_in
page, 'input[type="password"][name="passwd"]'
):
await _input_password(page, password)
elif await _check_for_visible_element(page, 'input[type="tel"][name="otc"]'):
elif await _check_for_visible_element(page, 'input[name="otc"]'):
await _input_code(page, None)
elif await _check_for_visible_element(
page, 'input[type="checkbox"][name="DontShowAgain"]'
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
click==7.1.2
requests==2.24.0
boto3==1.14.57
bs4==0.0.1
pyppeteer==0.2.6
asyncio==3.4.3
typer==0.4.1
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

setup(
name="auther",
version="0.0.4",
version="0.0.5",
author="Kamran Ali",
author_email="[email protected]",
description="Command line tool for AWS CLI authentication",
Expand All @@ -22,7 +22,7 @@
],
packages=[package for package in find_namespace_packages('.') if 'auther' in package],
install_requires=[
"Click",
"typer",
'requests',
'boto3',
'bs4',
Expand All @@ -31,6 +31,6 @@
],
entry_points="""
[console_scripts]
auther=auther.cli:main
auther=auther.cli:app
""",
)

0 comments on commit d21e548

Please sign in to comment.