Skip to content

Commit

Permalink
OIE authorization code flow implementation with forced classic authen…
Browse files Browse the repository at this point in the history
…tication. (#132)
  • Loading branch information
sevignyj authored Nov 15, 2023
1 parent 4333d94 commit eba165e
Show file tree
Hide file tree
Showing 13 changed files with 679 additions and 194 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ venv/
ENV/
env.bak/
venv.bak/
.vscode
.vscode/

# Spyder project settings
.spyderproject
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ tokens into your local `~/.aws/credentials` file.

## What's new
See [Releases](https://github.com/dowjones/tokendito/releases) for a detailed Changelog.
### Tokendito 2.3.0
Version 2.3.0 of Tokendito introduces the following new features:
- Basic OIE support while forcing Classic mode.
- Misc bug fixes

Note: This feature currently works with locally enabled OIE organizations, but it does not for Organizations with chained Authentication in mixed OIE/Classic environments.


### Tokendito 2.2.0

Version 2.2.0 of Tokendito introduces the following new features:
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ options:
--okta-org OKTA_ORG Set the Okta Org base URL. This enables role auto-discovery
--okta-tile OKTA_TILE
Okta tile URL to use.
--okta-client-id OKTA_CLIENT_ID
Sets the Okta client ID used in OAuth2. If passed, the authorize code flow will run.
--okta-mfa OKTA_MFA Sets the MFA method
--okta-mfa-response OKTA_MFA_RESPONSE
Sets the MFA response to a challenge
Expand Down
32 changes: 18 additions & 14 deletions tests/unit/test_okta.py
Original file line number Diff line number Diff line change
Expand Up @@ -470,11 +470,10 @@ def test_send_saml_request(mocker):
)

saml_request = {"relay_state": "relay_state", "request": "request", "post_url": "post_url"}
cookie = {"sid": "pytestcookie"}

mocker.patch("tokendito.http_client.HTTP_client.get", return_value=mock_response)

assert okta.send_saml_request(saml_request, cookie) == {
assert okta.send_saml_request(saml_request) == {
"response": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4=",
"relay_state": "foobar",
"post_url": "https://acme.okta.com/app/okta_org2org/akjlkjlksjx0xmdd/sso/saml",
Expand All @@ -484,6 +483,7 @@ def test_send_saml_request(mocker):
def test_send_saml_response(mocker):
"""Test sending SAML response."""
from tokendito import okta
from tokendito.config import Config
from tokendito.http_client import HTTP_client

mock_response = Mock()
Expand All @@ -495,12 +495,16 @@ def test_send_saml_response(mocker):
"post_url": "https://acme.okta.com/app/okta_org2org/akjlkjlksjx0xmdd/sso/saml",
}

mocker.patch("tokendito.okta.extract_state_token", return_value=None)

mocker.patch.object(HTTP_client, "post", return_value=mock_response)

assert okta.send_saml_response(saml_response) == mock_response.cookies
pytest_config = Config()

assert okta.send_saml_response(pytest_config, saml_response) == mock_response.cookies


def test_authenticate(mocker):
def test_idp_auth(mocker):
"""Test authentication."""
from tokendito import okta
from tokendito.config import Config
Expand All @@ -513,23 +517,23 @@ def test_authenticate(mocker):
}
)
sid = {"sid": "pytestsid"}
mocker.patch("tokendito.user.request_cookies", return_value=sid)
mocker.patch("tokendito.okta.local_auth", return_value="foobar")
mocker.patch("tokendito.okta.saml2_auth", return_value=sid)
mocker.patch("tokendito.okta.create_authn_cookies", return_value=sid)
mocker.patch("tokendito.okta.local_authenticate", return_value="foobar")
mocker.patch("tokendito.okta.saml2_authenticate", return_value=sid)

mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "OKTA"})
assert okta.authenticate(pytest_config) == sid
assert okta.idp_auth(pytest_config) == sid

mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "SAML2"})
assert okta.authenticate(pytest_config) == sid
assert okta.idp_auth(pytest_config) == sid

mocker.patch("tokendito.okta.get_auth_properties", return_value={"type": "UNKNOWN"})
with pytest.raises(SystemExit) as error:
assert okta.authenticate(pytest_config) == error
assert okta.idp_auth(pytest_config) == error

mocker.patch("tokendito.okta.get_auth_properties", return_value={})
with pytest.raises(SystemExit) as error:
assert okta.authenticate(pytest_config) == error
assert okta.idp_auth(pytest_config) == error


def test_step_up_authenticate(mocker):
Expand Down Expand Up @@ -605,7 +609,7 @@ def test_local_auth(mocker):
assert okta.local_auth(pytest_config) == "pytesttoken"


def test_saml2_auth(mocker):
def test_saml2_authenticate(mocker):
"""Test saml2 authentication."""
from tokendito import okta
from tokendito.config import Config
Expand All @@ -623,12 +627,12 @@ def test_saml2_auth(mocker):
"base_url": "https://acme.okta.com",
}
mocker.patch("tokendito.okta.get_saml_request", return_value=saml_request)
mocker.patch("tokendito.okta.authenticate", return_value="pytestsessioncookie")
mocker.patch("tokendito.okta.idp_auth", return_value="pytestsessioncookie")

saml_response = {
"response": "pytestresponse",
}

mocker.patch("tokendito.okta.send_saml_request", return_value=saml_response)
mocker.patch("tokendito.okta.send_saml_response", return_value="pytestsessionid")
assert okta.saml2_auth(pytest_config, auth_properties) == "pytestsessionid"
assert okta.saml2_authenticate(pytest_config, auth_properties) == "pytestsessionid"
2 changes: 1 addition & 1 deletion tokendito/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# vim: set filetype=python ts=4 sw=4
# -*- coding: utf-8 -*-
"""Tokendito module initialization."""
__version__ = "2.2.0"
__version__ = "2.3.0"
__title__ = "tokendito"
__description__ = "Get AWS STS tokens from Okta SSO"
__long_description_content_type__ = "text/markdown"
Expand Down
4 changes: 2 additions & 2 deletions tokendito/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ def main(args=None): # needed for console script

path = os.path.dirname(os.path.dirname(__file__))
sys.path[0:0] = [path]
from tokendito.tool import cli
from tokendito.user import cmd_interface

try:
return cli(args)
return cmd_interface(args)
except KeyboardInterrupt:
print("\nInterrupted")
sys.exit(1)
Expand Down
9 changes: 1 addition & 8 deletions tokendito/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,30 +47,23 @@ def get_output_types():
return ["json", "text", "csv", "yaml", "yaml-stream"]


def authenticate_to_roles(config, urls, cookies=None):
def authenticate_to_roles(config, urls, cookies):
"""Authenticate AWS user with saml.
:param urls: list of tuples or tuple, with tiles info
:param cookies: html cookies
:param user_agent: optional user agent string
:return: response text
"""
if cookies:
HTTP_client.set_cookies(cookies) # Set cookies if provided

url_list = [urls] if isinstance(urls, tuple) else urls
responses = []
tile_count = len(url_list)
plural = "s" if tile_count > 1 else ""

logger.info(f"Discovering roles in {tile_count} tile{plural}.")
for url, label in url_list:
response = HTTP_client.get(url) # Use the HTTPClient's get method

session_url = config.okta["org"] + "/login/sessionCookieRedirect"
params = {"token": cookies.get("sessionToken"), "redirectUrl": url}

response = HTTP_client.get(session_url, params=params)

saml_response_string = response.text
Expand Down
1 change: 1 addition & 0 deletions tokendito/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class Config(object):
password="",
mfa=None,
mfa_response=None,
client_id=None,
tile=None,
org=None,
device_token=None,
Expand Down
8 changes: 6 additions & 2 deletions tokendito/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ def set_cookies(self, cookies):
"""Update session with additional cookies."""
self.session.cookies.update(cookies)

def get(self, url, params=None, headers=None):
def get(self, url, params=None, headers=None, allow_redirects=True):
"""Perform a GET request."""
response = None
try:
logger.debug(f"GET to {url}")
logger.debug(f"Sending cookies: {self.session.cookies}")
logger.debug(f"Sending headers: {self.session.headers}")
response = self.session.get(url, params=params, headers=headers)
response = self.session.get(
url, params=params, headers=headers, allow_redirects=allow_redirects
)
response.raise_for_status()
logger.debug(f"Received response from {url}: {response.text}")
return response
Expand All @@ -51,6 +54,7 @@ def get(self, url, params=None, headers=None):

def post(self, url, data=None, json=None, headers=None, params=None, return_json=False):
"""Perform a POST request."""
logger.debug(f"POST to {url}")
try:
response = self.session.post(url, data=data, json=json, params=params, headers=headers)
response.raise_for_status()
Expand Down
Loading

0 comments on commit eba165e

Please sign in to comment.