vhi-python 0.1.1__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vhi_python-0.1.1 → vhi_python-0.1.3}/PKG-INFO +2 -2
- {vhi_python-0.1.1 → vhi_python-0.1.3}/pyproject.toml +2 -2
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi/client.py +49 -29
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi_python.egg-info/PKG-INFO +2 -2
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi_python.egg-info/SOURCES.txt +2 -1
- {vhi_python-0.1.1 → vhi_python-0.1.3}/tests/test_client.py +14 -10
- vhi_python-0.1.3/tests/test_integration.py +31 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/README.md +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/setup.cfg +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi/__init__.py +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi/exceptions.py +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi/models.py +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi_python.egg-info/dependency_links.txt +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi_python.egg-info/requires.txt +0 -0
- {vhi_python-0.1.1 → vhi_python-0.1.3}/src/vhi_python.egg-info/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vhi-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads.
|
|
5
5
|
Author-email: Agent <agent@example.com>
|
|
6
|
-
License
|
|
6
|
+
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/agent/vhi-python
|
|
8
8
|
Project-URL: Repository, https://github.com/agent/vhi-python.git
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vhi-python"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [
|
|
11
11
|
{ name = "Agent", email = "agent@example.com" }
|
|
12
12
|
]
|
|
13
|
-
license = "MIT"
|
|
13
|
+
license = { text = "MIT" }
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
import os
|
|
3
|
+
import uuid
|
|
4
|
+
import json
|
|
3
5
|
from typing import Callable, Optional, List
|
|
4
6
|
from .exceptions import VhiAuthenticationError, VhiMfaRequiredError, VhiApiError
|
|
5
7
|
from .models import ClaimStatement
|
|
@@ -29,8 +31,10 @@ class VhiClient:
|
|
|
29
31
|
|
|
30
32
|
# Default Browser Headers to mimic browser and avoid WAF
|
|
31
33
|
self.session.headers.update({
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
+
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
35
|
+
"sec-ch-ua": '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
|
|
36
|
+
"sec-ch-ua-mobile": "?0",
|
|
37
|
+
"sec-ch-ua-platform": '"Windows"',
|
|
34
38
|
})
|
|
35
39
|
|
|
36
40
|
self.is_authenticated = False
|
|
@@ -52,53 +56,70 @@ class VhiClient:
|
|
|
52
56
|
"""
|
|
53
57
|
Performs the login flow. Handles the MFA challenge if required.
|
|
54
58
|
"""
|
|
55
|
-
# Set proper fetch headers
|
|
59
|
+
# Set proper fetch headers mimicking trace exactly
|
|
56
60
|
headers = {
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
61
|
+
"accept": "application/json, text/plain, */*",
|
|
62
|
+
"accept-language": "en-IE,en-US;q=0.9,en;q=0.8,ja;q=0.7,en-GB;q=0.6,de;q=0.5",
|
|
63
|
+
"cache-control": "no-cache",
|
|
64
|
+
"content-type": "application/json",
|
|
65
|
+
"dnt": "1",
|
|
66
|
+
"origin": "https://app.vhi.ie",
|
|
67
|
+
"pragma": "no-cache",
|
|
68
|
+
"priority": "u=1, i",
|
|
69
|
+
"referer": "https://app.vhi.ie/",
|
|
70
|
+
"sec-fetch-dest": "empty",
|
|
71
|
+
"sec-fetch-mode": "cors",
|
|
72
|
+
"sec-fetch-site": "same-site",
|
|
73
|
+
"user-session-id": str(uuid.uuid4())
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
payload = {
|
|
66
77
|
"username": self.username,
|
|
67
|
-
"
|
|
78
|
+
"usercred": self.password
|
|
68
79
|
}
|
|
69
80
|
|
|
81
|
+
# Format strictly without spaces to bypass strict WAF parsers
|
|
82
|
+
raw_payload = json.dumps(payload, separators=(',', ':'))
|
|
83
|
+
|
|
70
84
|
# 1. Primary Authentication
|
|
71
|
-
|
|
72
|
-
login_url = f"{self.apis_base_url}/auth/v1/login"
|
|
85
|
+
login_url = f"{self.apis_base_url}/api/myvhilogin/login"
|
|
73
86
|
|
|
74
|
-
response = self.session.post(login_url,
|
|
87
|
+
response = self.session.post(login_url, data=raw_payload, headers=headers)
|
|
75
88
|
|
|
76
89
|
if response.status_code == 200:
|
|
77
|
-
self.is_authenticated = True
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
elif response.status_code == 202:
|
|
81
|
-
# MFA Challenge Required
|
|
82
90
|
try:
|
|
83
91
|
data = response.json()
|
|
84
92
|
except ValueError:
|
|
85
|
-
raise VhiApiError("
|
|
93
|
+
raise VhiApiError("Login response was not valid JSON", status_code=response.status_code)
|
|
86
94
|
|
|
87
|
-
|
|
88
|
-
|
|
95
|
+
# The HAR trace uses a deeply nested response structure or a flattened one depending on Gateway
|
|
96
|
+
status_val = data.get("status") or data.get("data", {}).get("status")
|
|
97
|
+
|
|
98
|
+
if status_val == "MFA_REQUIRED":
|
|
99
|
+
state_token = data.get("stateToken") or data.get("data", {}).get("stateToken")
|
|
100
|
+
|
|
101
|
+
# Attempt to extract the verify url from the first factor
|
|
102
|
+
try:
|
|
103
|
+
factors = data.get("factors") or data.get("data", {}).get("_embedded", {}).get("factors", [])
|
|
104
|
+
verify_url = factors[0]["_links"]["verify"]["href"]
|
|
105
|
+
except (IndexError, KeyError):
|
|
106
|
+
# Fallback URL if extraction fails
|
|
107
|
+
verify_url = f"https://admin-digital.vhi.ie/api/v1/authn/factors/verify"
|
|
108
|
+
|
|
89
109
|
if not state_token:
|
|
90
110
|
raise VhiApiError("MFA required but no state_token provided in response")
|
|
91
111
|
|
|
92
|
-
self._handle_mfa_challenge(state_token, headers)
|
|
112
|
+
self._handle_mfa_challenge(state_token, verify_url, headers)
|
|
93
113
|
else:
|
|
94
|
-
|
|
114
|
+
self.is_authenticated = True
|
|
115
|
+
return
|
|
95
116
|
|
|
96
117
|
elif response.status_code == 401:
|
|
97
118
|
raise VhiAuthenticationError("Invalid credentials provided.")
|
|
98
119
|
else:
|
|
99
|
-
raise VhiApiError(f"Login failed with status {response.status_code}", status_code=response.status_code, response=response)
|
|
120
|
+
raise VhiApiError(f"Login failed with status {response.status_code}: {response.text}", status_code=response.status_code, response=response)
|
|
100
121
|
|
|
101
|
-
def _handle_mfa_challenge(self, state_token: str, base_headers: dict):
|
|
122
|
+
def _handle_mfa_challenge(self, state_token: str, verify_url: str, base_headers: dict):
|
|
102
123
|
"""
|
|
103
124
|
Internal method to submit the MFA code.
|
|
104
125
|
"""
|
|
@@ -111,13 +132,12 @@ class VhiClient:
|
|
|
111
132
|
if not otp_code:
|
|
112
133
|
raise VhiAuthenticationError("MFA callback did not return a valid OTP code.")
|
|
113
134
|
|
|
114
|
-
mfa_url = f"{self.apis_base_url}/auth/v1/mfa/verify"
|
|
115
135
|
payload = {
|
|
116
|
-
"
|
|
117
|
-
"
|
|
136
|
+
"passCode": otp_code,
|
|
137
|
+
"stateToken": state_token
|
|
118
138
|
}
|
|
119
139
|
|
|
120
|
-
response = self.session.post(
|
|
140
|
+
response = self.session.post(verify_url, json=payload, headers=base_headers)
|
|
121
141
|
|
|
122
142
|
if response.status_code == 200:
|
|
123
143
|
self.is_authenticated = True
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vhi-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A Python client library for the Vhi API, handling authentication, MFA callbacks, claims retrieval, and document downloads.
|
|
5
5
|
Author-email: Agent <agent@example.com>
|
|
6
|
-
License
|
|
6
|
+
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/agent/vhi-python
|
|
8
8
|
Project-URL: Repository, https://github.com/agent/vhi-python.git
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -11,8 +11,8 @@ def test_login_success_no_mfa():
|
|
|
11
11
|
|
|
12
12
|
responses.add(
|
|
13
13
|
responses.POST,
|
|
14
|
-
"https://apis.vhi.ie/
|
|
15
|
-
json={"
|
|
14
|
+
"https://apis.vhi.ie/api/myvhilogin/login",
|
|
15
|
+
json={"status": "SUCCESS", "sessionToken": "test-token"},
|
|
16
16
|
status=200
|
|
17
17
|
)
|
|
18
18
|
|
|
@@ -26,19 +26,23 @@ def test_login_requires_mfa_success():
|
|
|
26
26
|
|
|
27
27
|
client = VhiClient("test@example.com", "password", mfa_callback=mfa_callback)
|
|
28
28
|
|
|
29
|
-
# Mock primary login returning
|
|
29
|
+
# Mock primary login returning 200 with MFA_REQUIRED
|
|
30
30
|
responses.add(
|
|
31
31
|
responses.POST,
|
|
32
|
-
"https://apis.vhi.ie/
|
|
33
|
-
json={
|
|
34
|
-
|
|
32
|
+
"https://apis.vhi.ie/api/myvhilogin/login",
|
|
33
|
+
json={
|
|
34
|
+
"status": "MFA_REQUIRED",
|
|
35
|
+
"stateToken": "state-123",
|
|
36
|
+
"factors": [{"_links": {"verify": {"href": "https://admin-digital.vhi.ie/api/v1/authn/factors/verify"}}}]
|
|
37
|
+
},
|
|
38
|
+
status=200
|
|
35
39
|
)
|
|
36
40
|
|
|
37
|
-
# Mock MFA verify returning 200
|
|
41
|
+
# Mock MFA verify returning 200 SUCCESS
|
|
38
42
|
responses.add(
|
|
39
43
|
responses.POST,
|
|
40
|
-
"https://
|
|
41
|
-
json={"
|
|
44
|
+
"https://admin-digital.vhi.ie/api/v1/authn/factors/verify",
|
|
45
|
+
json={"status": "SUCCESS", "sessionToken": "final-token"},
|
|
42
46
|
status=200
|
|
43
47
|
)
|
|
44
48
|
|
|
@@ -51,7 +55,7 @@ def test_login_invalid_credentials():
|
|
|
51
55
|
|
|
52
56
|
responses.add(
|
|
53
57
|
responses.POST,
|
|
54
|
-
"https://apis.vhi.ie/
|
|
58
|
+
"https://apis.vhi.ie/api/myvhilogin/login",
|
|
55
59
|
status=401
|
|
56
60
|
)
|
|
57
61
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pytest
|
|
3
|
+
from vhi.client import VhiClient
|
|
4
|
+
from vhi.exceptions import VhiMfaRequiredError, VhiAuthenticationError
|
|
5
|
+
|
|
6
|
+
@pytest.mark.skipif(
|
|
7
|
+
not os.getenv("VHI_USERNAME") or not os.getenv("VHI_PASSWORD"),
|
|
8
|
+
reason="Integration tests require VHI_USERNAME and VHI_PASSWORD environment variables"
|
|
9
|
+
)
|
|
10
|
+
def test_live_login():
|
|
11
|
+
"""
|
|
12
|
+
Test live login against Vhi API.
|
|
13
|
+
We expect to hit either a successful login or an MFA challenge.
|
|
14
|
+
"""
|
|
15
|
+
username = os.environ["VHI_USERNAME"]
|
|
16
|
+
password = os.environ["VHI_PASSWORD"]
|
|
17
|
+
|
|
18
|
+
# We initialize without an mfa_callback to ensure it raises VhiMfaRequiredError
|
|
19
|
+
# if MFA is prompted, which validates the primary credentials were correct.
|
|
20
|
+
client = VhiClient(username, password)
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
client.login()
|
|
24
|
+
# If it succeeds without MFA, we are authenticated.
|
|
25
|
+
assert client.is_authenticated is True
|
|
26
|
+
except VhiMfaRequiredError:
|
|
27
|
+
# If it throws MFA required, the API accepted the usercred and triggered Okta MFA.
|
|
28
|
+
# This confirms our primary login endpoint and payload reverse-engineering is correct.
|
|
29
|
+
pass
|
|
30
|
+
except VhiAuthenticationError as e:
|
|
31
|
+
pytest.fail(f"Integration login failed - incorrect credentials: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|