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.
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vhi-python
3
- Version: 0.1.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-Expression: MIT
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.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
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
33
- "Accept-Language": "en-US,en;q=0.9",
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
- "Content-Type": "application/json",
58
- "Accept": "application/json",
59
- "Origin": "https://www.vhi.ie",
60
- "Referer": "https://www.vhi.ie/",
61
- "Sec-Fetch-Mode": "cors",
62
- "Sec-Fetch-Site": "same-site",
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
- "password": self.password
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
- # The path here is a presumed standard, will likely be updated if the actual path varies
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, json=payload, headers=headers)
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("MFA response was not valid JSON", status_code=response.status_code)
93
+ raise VhiApiError("Login response was not valid JSON", status_code=response.status_code)
86
94
 
87
- if data.get("mfa_required"):
88
- state_token = data.get("state_token")
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
- raise VhiApiError("Received 202 but not MFA challenge", status_code=response.status_code, response=response)
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
- "otp": otp_code,
117
- "state_token": state_token
136
+ "passCode": otp_code,
137
+ "stateToken": state_token
118
138
  }
119
139
 
120
- response = self.session.post(mfa_url, json=payload, headers=base_headers)
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.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-Expression: MIT
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
@@ -9,4 +9,5 @@ src/vhi_python.egg-info/SOURCES.txt
9
9
  src/vhi_python.egg-info/dependency_links.txt
10
10
  src/vhi_python.egg-info/requires.txt
11
11
  src/vhi_python.egg-info/top_level.txt
12
- tests/test_client.py
12
+ tests/test_client.py
13
+ tests/test_integration.py
@@ -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/auth/v1/login",
15
- json={"token": "test-token"},
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 202
29
+ # Mock primary login returning 200 with MFA_REQUIRED
30
30
  responses.add(
31
31
  responses.POST,
32
- "https://apis.vhi.ie/auth/v1/login",
33
- json={"mfa_required": True, "state_token": "state-123"},
34
- status=202
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://apis.vhi.ie/auth/v1/mfa/verify",
41
- json={"token": "final-token"},
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/auth/v1/login",
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