Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.8.0
- Add `to_rsa_format` function to normalize private key
- Update requests_mauth and httpx_mauth to support reading configuration from environment variables

# 1.7.0
- Add `MAuthHttpx` custom authentication scheme for HTTPX.
- Remove Support for EOL Python 3.8
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ client = httpx.Client(auth=auth)
response = client.get("https://api.example.com/endpoint")
```

The following variables can be configured in the environment variables:

| Key | Value |
| ------------------------------------ | ---------------------------------- |
| `APP_UUID` or `MAUTH_APP_UUID` | APP_UUID for signing requests |
| `PRIVATE_KEY` or `MAUTH_PRIVATE_KEY` | MAuth private key for the APP_UUID |

The `mauth_sign_versions` option can be set as an environment variable to specify protocol versions to sign outgoing requests:

| Key | Value |
Expand All @@ -103,11 +110,11 @@ MAuth Client Python supports AWS Lambda functions and Flask applications to auth

The following variables are **required** to be configured in the environment variables:

| Key | Value |
| -------------- | ------------------------------------------------------------- |
| `APP_UUID` | APP_UUID for the AWS Lambda function |
| `PRIVATE_KEY` | Encrypted private key for the APP_UUID |
| `MAUTH_URL` | MAuth service URL (e.g. https://mauth-innovate.imedidata.com) |
| Key | Value |
| ------------------------------------ | ------------------------------------------------------------- |
| `APP_UUID` or `MAUTH_APP_UUID` | APP_UUID for the AWS Lambda function |
| `PRIVATE_KEY` or `MAUTH_PRIVATE_KEY` | Encrypted private key for the APP_UUID |
| `MAUTH_URL` | MAuth service URL (e.g. https://mauth-innovate.imedidata.com) |


The following variables can optionally be set in the environment variables:
Expand Down
15 changes: 8 additions & 7 deletions mauth_client/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import os
from .utils import to_rsa_format


class Config:
APP_UUID = os.environ.get("APP_UUID")
MAUTH_URL = os.environ.get("MAUTH_URL")
MAUTH_API_VERSION = os.environ.get("MAUTH_API_VERSION", "v1")
MAUTH_MODE = os.environ.get("MAUTH_MODE", "local")
PRIVATE_KEY = os.environ.get("PRIVATE_KEY")
V2_ONLY_AUTHENTICATE = str(os.environ.get("V2_ONLY_AUTHENTICATE")).lower() == "true"
SIGN_VERSIONS = os.environ.get("MAUTH_SIGN_VERSIONS", "v1")
APP_UUID = os.getenv("APP_UUID", os.getenv("MAUTH_APP_UUID"))
MAUTH_URL = os.getenv("MAUTH_URL")
MAUTH_API_VERSION = os.getenv("MAUTH_API_VERSION", "v1")
MAUTH_MODE = os.getenv("MAUTH_MODE", "local")
PRIVATE_KEY = to_rsa_format(os.getenv("PRIVATE_KEY", os.getenv("MAUTH_PRIVATE_KEY", "")))
Comment thread
ykitamura-mdsol marked this conversation as resolved.
Outdated
V2_ONLY_AUTHENTICATE = str(os.getenv("V2_ONLY_AUTHENTICATE")).lower() == "true"
SIGN_VERSIONS = os.getenv("MAUTH_SIGN_VERSIONS", "v1")
4 changes: 2 additions & 2 deletions mauth_client/httpx_mauth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class MAuthHttpx(httpx.Auth):

def __init__(
self,
app_uuid: str,
private_key_data: str,
app_uuid: str = Config.APP_UUID,
private_key_data: str = Config.PRIVATE_KEY,
sign_versions: str = Config.SIGN_VERSIONS,
):
self.signer = Signer(app_uuid, private_key_data, sign_versions)
Expand Down
7 changes: 6 additions & 1 deletion mauth_client/requests_mauth/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ class MAuth(requests.auth.AuthBase):
Custom requests authorizer for MAuth
"""

def __init__(self, app_uuid, private_key_data, sign_versions=Config.SIGN_VERSIONS):
def __init__(
self,
app_uuid=Config.APP_UUID,
private_key_data=Config.PRIVATE_KEY,
sign_versions=Config.SIGN_VERSIONS
):
"""
Create a new MAuth Instance

Expand Down
22 changes: 22 additions & 0 deletions mauth_client/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import base64
import charset_normalizer
import re
from hashlib import sha512

HEADER = '-----BEGIN RSA PRIVATE KEY-----'
FOOTER = '-----END RSA PRIVATE KEY-----'


def make_bytes(val):
"""
Expand Down Expand Up @@ -32,3 +36,21 @@ def decode(byte_string: bytes) -> str:
except UnicodeDecodeError:
encoding = charset_normalizer.detect(byte_string)["encoding"]
return byte_string.decode(encoding)


def to_rsa_format(key: str) -> str:
"""Convert a private key to RSA format with proper newlines."""

if "\n" in key:
Comment thread
ykitamura-mdsol marked this conversation as resolved.
Outdated
return key

body = key.strip()
body = body.replace(HEADER, "").replace(FOOTER, "").strip()

# Replace whitespace with newlines or chunk into 64-char lines
if " " in body or "\t" in body:
body = re.sub(r'\s+', '\n', body)
else:
Comment thread
ykitamura-mdsol marked this conversation as resolved.
body = '\n'.join(body[i:i + 64] for i in range(0, len(body), 64))

return f"{HEADER}\n{body}\n{FOOTER}"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "mauth-client"
version = "1.7.0"
version = "1.8.0"
description = "MAuth Client for Python"
repository = "https://github.com/mdsol/mauth-client-python"
authors = ["Medidata Solutions <support@mdsol.com>"]
Expand Down
13 changes: 5 additions & 8 deletions tests/httpx_mauth/client_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import unittest
import os
import httpx
from mauth_client.httpx_mauth import MAuthHttpx
from ..common import load_key

APP_UUID = "5ff4257e-9c16-11e0-b048-0026bbfffe5e"
PRIVATE_KEY = load_key("priv")
URL = "https://innovate.imedidata.com/api/v2/users/10ac3b0e-9fe2-11df-a531-12313900d531/studies.json"


Expand All @@ -12,28 +13,24 @@ def handler(request):


class MAuthHttpxBaseTest(unittest.TestCase):
def setUp(self):
with open(os.path.join(os.path.dirname(__file__), "..", "keys", "fake_mauth.priv.key"), "r") as key_file:
self.example_private_key = key_file.read()

def test_call(self):
auth = MAuthHttpx(APP_UUID, self.example_private_key, sign_versions="v1,v2")
auth = MAuthHttpx(APP_UUID, PRIVATE_KEY, sign_versions="v1,v2")
with httpx.Client(transport=httpx.MockTransport(handler), auth=auth) as client:
response = client.get(URL)

for header in ["mcc-authentication", "mcc-time", "x-mws-authentication", "x-mws-time"]:
self.assertIn(header, response.request.headers)

def test_call_v1_only(self):
auth = MAuthHttpx(APP_UUID, self.example_private_key)
auth = MAuthHttpx(APP_UUID, PRIVATE_KEY)
with httpx.Client(transport=httpx.MockTransport(handler), auth=auth) as client:
response = client.get(URL)

for header in ["x-mws-authentication", "x-mws-time"]:
self.assertIn(header, response.request.headers)

def test_call_v2_only(self):
auth = MAuthHttpx(APP_UUID, self.example_private_key, sign_versions="v2")
auth = MAuthHttpx(APP_UUID, PRIVATE_KEY, sign_versions="v2")
with httpx.Client(transport=httpx.MockTransport(handler), auth=auth) as client:
response = client.get(URL)

Expand Down
22 changes: 22 additions & 0 deletions tests/utils_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import unittest

from .common import load_key
from mauth_client.utils import to_rsa_format

PRIVATE_KEY = load_key("priv").strip()


class TestToRsaFormat(unittest.TestCase):
def test_proper_format(self):
key = to_rsa_format(PRIVATE_KEY)
self.assertEqual(key, PRIVATE_KEY)

def test_newlines_replaced_with_spaces(self):
key_no_newlines = PRIVATE_KEY.replace("\n", " ")
key = to_rsa_format(key_no_newlines)
self.assertEqual(key, PRIVATE_KEY)

def test_newlines_removed(self):
key_no_newlines = PRIVATE_KEY.replace("\n", "")
key = to_rsa_format(key_no_newlines)
self.assertEqual(key, PRIVATE_KEY)