Skip to content

Commit

Permalink
Merge pull request #411 from Nike-Inc/dev
Browse files Browse the repository at this point in the history
Merge changes from Dev branch
  • Loading branch information
epierce authored Jun 20, 2023
2 parents 1019a08 + fc5219d commit cc29fc5
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 123 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ source ${INSTALL_DIR}/gimme-aws-creds-autocomplete.sh
## Using gimme-aws-creds with Okta Identity Engine
There are two options for using gimme-aws-creds with an OIE domain:
* Device Authorization Flow
* Forcing the use of the Okta Classic login flow
### Okta Identity Engine and Device Authorization Flow
This is the recommended method for authentication with OIE. It matches the flow used by Okta's [AWS client](https://github.com/okta/okta-aws-cli). When using gimme-aws-creds with the Device Authorization flow, you will authenticate using your browser. Storing credentials in keychain or passing MFA codes through the command-line is NOT POSSIBLE.
To use gimme-aws-creds with an Okta Identity Engine (OIE) domain, you must create a new OIDC Native Application and connect it to your AWS integration app(s).
The OIDC Native Application requires Grant Types `Authorization Code`, `Device Authorization` , and `Token Exchange`. These settings are in the Okta Admin UI at `Applications > [the OIDC app] > General Settings > Grant type`.
Expand All @@ -165,7 +172,12 @@ The pairing with the AWS Federation Application is achieved in the Fed app's Sig
Finally, set the Client ID in gimme-aws-creds (`gimme-aws-creds --action-configure` or update the `client_id` parameter in your config file)
**When using gimme-aws-creds with an OIE domain, you will authenticate using your browser. Storing credentials in keychain or passing MFA codes through the command-line is NOT POSSIBLE.**
### Forcing the use of the Okta Classic login flow ###
The login flow used in Okta Classic currently still works with Okta Identity Engine domains, BUT there are a couple caveats:
* The Okta classic flow passes the `stateToken` parameter when requesting "step-up" authentication. This capability was removed in OIE, so if the authentication policy on your AWS app(s) requires MFA but the Global Session Policy does not (or if a higher level of MFA factor is required to access AWS), you cannot authenticate using the classic login flow.
* MFA using Okta Verify is only supported on mobile devices. Okta Verify on macOS/Windows is not supported.
* Passwordless authentication and endpoint security checks are not supported.
## Configuration
Expand Down Expand Up @@ -217,6 +229,7 @@ A configuration wizard will prompt you to enter the necessary configuration para
- remember_device - y or n. If yes, the MFA device will be remembered by Okta service for a limited time. This option can also be set interactively in the command line using `-m` or `--remember-device`
- output_format - `json` , `export` or `windows`, determines default credential output format, can be also specified by `--output-format FORMAT` and `-o FORMAT`.
- open-browser - Open the device authentication link in the default web browser automatically (Okta Identity Engine domains only)
- force-classic - Force the use of the Okta Classic login process (Okta Identity Engine domains only)
## Configuration File
Expand Down Expand Up @@ -298,6 +311,7 @@ A list of values of to change with environment variables are:
- `OKTA_MFA_CODE` - corresponds to `--mfa-code` CLI option
- `OKTA_PASSWORD` - provides password during authentication, can be used in CI
- `OKTA_USERNAME` - corresponds to `okta_username` configuration and `--username` CLI option
- `AWS_STS_REGION` - force the use of the STS in a specific region (`us-east-1`, `eu-north-1`, etc.)
Example: `GIMME_AWS_CREDS_CLIENT_ID='foobar' AWS_DEFAULT_DURATION=12345 gimme-aws-creds`
Expand Down
111 changes: 89 additions & 22 deletions gimme_aws_creds/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import argparse
import configparser
import os
import requests
from urllib.parse import urlparse

from . import errors, ui, version
Expand Down Expand Up @@ -55,6 +56,7 @@ def __init__(self, gac_ui, create_config=True):
self.action_setup_fido_authenticator = False
self.action_output_format = False
self.output_format = 'export'
self.force_classic = False
self.roles = []

if self.ui.environ.get("OKTA_USERNAME") is not None:
Expand Down Expand Up @@ -150,6 +152,10 @@ def get_args(self):
'--open-browser', action='store_true',
help='Automatically open a webbrowser for device authorization (Okta Identity Engine only)'
)
parser.add_argument(
'--force-classic', action='store_true',
help='Force the use of the Okta Classic login process (Okta Identity Engine only)'
)
args = parser.parse_args(self.ui.args)

self.action_configure = args.action_configure
Expand All @@ -159,6 +165,7 @@ def get_args(self):
self.action_register_device = args.action_register_device
self.action_setup_fido_authenticator = args.action_setup_fido_authenticator
self.open_browser = args.open_browser
self.force_classic = args.force_classic

if args.insecure is True:
ui.default.warning("Warning: SSL certificate validation is disabled!")
Expand Down Expand Up @@ -254,6 +261,8 @@ def update_config_file(self):
'remember_device': 'n',
'aws_default_duration': '3600',
'output_format': 'export',
'force_classic': '',
'open_browser': ''
}

# See if a config file already exists.
Expand All @@ -270,24 +279,38 @@ def update_config_file(self):
# Prompt user for config details and store in config_dict
config_dict = defaults
config_dict['okta_org_url'] = self._get_org_url_entry(defaults['okta_org_url'])
client_id_set = False

# Options specific to OIE domains
if self._okta_platform == 'identity_engine':
config_dict['force_classic'] = self._get_force_classic(defaults['force_classic'])
if config_dict['force_classic'] is False:
config_dict['open_browser'] = self._get_open_browser(defaults['open_browser'])
config_dict['client_id'] = self._get_client_id_entry(defaults['client_id'])
client_id_set = True

# These options are only used in the Classic authentication flow
if self._okta_platform == 'classic' or config_dict['force_classic'] is True:
config_dict['okta_username'] = self._get_okta_username(defaults['okta_username'])
config_dict['preferred_mfa_type'] = self._get_preferred_mfa_type(defaults['preferred_mfa_type'])
config_dict['remember_device'] = self._get_remember_device(defaults['remember_device'])

# The rest of the options are used in both OIE and Classic
config_dict['gimme_creds_server'] = self._get_gimme_creds_server_entry(defaults['gimme_creds_server'])

if config_dict['gimme_creds_server'] == 'appurl':
config_dict['app_url'] = self._get_appurl_entry(defaults['app_url'])
elif config_dict['gimme_creds_server'] != 'internal':
config_dict['client_id'] = self._get_client_id_entry(defaults['client_id'])
if client_id_set is False:
config_dict['client_id'] = self._get_client_id_entry(defaults['client_id'])
config_dict['okta_auth_server'] = self._get_auth_server_entry(defaults['okta_auth_server'])

config_dict['write_aws_creds'] = self._get_write_aws_creds(defaults['write_aws_creds'])
if config_dict['gimme_creds_server'] != 'appurl':
config_dict['aws_appname'] = self._get_aws_appname(defaults['aws_appname'])
config_dict['resolve_aws_alias'] = self._get_resolve_aws_alias(defaults['resolve_aws_alias'])
config_dict['include_path'] = self._get_include_path(defaults['include_path'])
config_dict['aws_rolename'] = self._get_aws_rolename(defaults['aws_rolename'])
config_dict['okta_username'] = self._get_okta_username(defaults['okta_username'])
config_dict['resolve_aws_alias'] = self._get_resolve_aws_alias(defaults['resolve_aws_alias'])
config_dict['aws_default_duration'] = self._get_aws_default_duration(defaults['aws_default_duration'])
config_dict['preferred_mfa_type'] = self._get_preferred_mfa_type(defaults['preferred_mfa_type'])
config_dict['remember_device'] = self._get_remember_device(defaults['remember_device'])
if config_dict['gimme_creds_server'] != 'appurl':
config_dict['aws_appname'] = self._get_aws_appname(defaults['aws_appname'])

config_dict["output_format"] = ''
if not config_dict["write_aws_creds"]:
config_dict['output_format'] = self._get_output_format(defaults['output_format'])
Expand Down Expand Up @@ -317,19 +340,39 @@ def _get_org_url_entry(self, default_entry):

while okta_org_url_valid is False:
okta_org_url = self._get_user_input("Okta URL for your organization", default_entry).strip('/')
# Validate that okta_org_url is a well formed okta URL
url_parse_results = urlparse(okta_org_url)
allowlist = [
"okta.com",
"oktapreview.com",
"okta-emea.com",
]

if url_parse_results.scheme == "https" and any(urlelement in url_parse_results.hostname for urlelement in allowlist):
okta_org_url_valid = True
# Validate that the URL given is an Okta domain and what the platform is
url_parse_results = urlparse(okta_org_url)
if url_parse_results.scheme == "https":
try:
response = requests.get(
okta_org_url + '/.well-known/okta-organization',
headers={
'Accept': 'application/json',
'User-Agent': "gimme-aws-creds {}".format(version)
},
timeout=30
)

response_data = response.json()

if response.status_code == 200:
if response_data['pipeline'] == 'v1':
self._okta_platform = 'classic'
okta_org_url_valid = True
ui.default.notify("Okta Classic domain detected")
elif response_data['pipeline'] == 'idx':
self._okta_platform = 'identity_engine'
okta_org_url_valid = True
ui.default.notify("Okta Identity Engine domain detected")
else:
ui.default.error('Unknown Okta platform type: {}'.format(response_data['pipeline']))
else:
response.raise_for_status()
except Exception as err:
ui.default.error('{} is not a valid Okta domain'.format(okta_org_url))
else:
ui.default.error(
"Okta organization URL must be HTTPS URL for okta.com or oktapreview.com or okta-emea.com domain")
ui.default.error("Okta organization URL must be HTTPS URL")

self._okta_org_url = okta_org_url

Expand Down Expand Up @@ -383,7 +426,7 @@ def _get_appurl_entry(self, default_entry):

def _get_gimme_creds_server_entry(self, default_entry):
""" Get gimme_creds_server """
ui.default.message("Enter the URL for the gimme-creds-server or 'internal' for handling Okta APIs locally.")
ui.default.message("Enter the URL for the gimme-creds-server, 'appurl' for an Okta Application URL or 'internal' for handling Okta APIs locally.")
gimme_creds_server_valid = False
gimme_creds_server = default_entry

Expand Down Expand Up @@ -488,7 +531,7 @@ def _get_conf_profile_name(self, default_entry):
def _get_okta_username(self, default_entry):
"""Get and validate okta username. [Optional]"""
ui.default.message(
"If you'd like to set your okta username in the config file, specify the username\n."
"If you'd like to set your okta username in the config file, specify the username.\n"
"This is optional.")
okta_username = self._get_user_input(
"Okta User Name", default_entry)
Expand Down Expand Up @@ -544,6 +587,30 @@ def _get_remember_device(self, default_entry):
except ValueError:
ui.default.warning("Remember the MFA device must be either y or n.")

def _get_force_classic(self, default_entry):
"""Option to force the Okta Classic login process"""
ui.default.message(
"Do you want to force the Okta Classic login flow? (Okta Identity Engine domains only)\n"
"Please answer y or n.")
while True:
try:
return self._get_user_input_yes_no(
"Force classic login flow", default_entry)
except ValueError:
ui.default.warning("Force Classic login flow must be either y or n.")

def _get_open_browser(self, default_entry):
"""Option to automatically open the default browser for OIE authentication"""
ui.default.message(
"Do you want to automatically open the default browser for authentication? (Okta Identity Engine domains only)\n"
"Please answer y or n.")
while True:
try:
return self._get_user_input_yes_no(
"Open default browser automatically", default_entry)
except ValueError:
ui.default.warning("Open browser must be either y or n.")

def _get_user_input(self, message, default=None):
"""formats message to include default and then prompts user for input
via keyboard with message. Returns user's input or if user doesn't
Expand Down
Loading

0 comments on commit cc29fc5

Please sign in to comment.