Skip to content

Commit 2a9c1bb

Browse files
Add action param to validate challenge (#24)
* Add action param to validate_challenge * Update version.py * Add test for invalid user_id in validate_challenge method * Fix test * Bump version * Replace duplicated user_id validate_challenge test with action test * Append /v1 to api_url if not provided. * Remove base_challenge_url from tests * Add v1 check to delete user
1 parent 0c28332 commit 2a9c1bb

File tree

4 files changed

+61
-28
lines changed

4 files changed

+61
-28
lines changed

authsignal/client.py

+39-18
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import decimal
2+
import authsignal
23
from authsignal.version import VERSION
34

45
import humps
@@ -9,8 +10,7 @@
910

1011
_UNICODE_STRING = str
1112

12-
API_BASE_URL = 'https://signal.authsignal.com'
13-
API_CHALLENGE_URL = 'https://api.authsignal.com/v1'
13+
API_BASE_URL = 'https://api.authsignal.com/v1'
1414

1515
BLOCK = "BLOCK"
1616
ALLOW = "ALLOW"
@@ -52,8 +52,8 @@ def __init__(
5252
self.url = api_url
5353
self.timeout = timeout
5454
self.version = version
55+
self.api_version = 'v1'
5556

56-
5757
def track(self, user_id, action, payload=None, path=None):
5858
"""Tracks an action to authsignal, scoped to the user_id and action
5959
Returns the status of the action so that you can determine to whether to continue
@@ -152,8 +152,7 @@ def get_user(self, user_id, redirect_url=None, path=None):
152152
def delete_user(self, user_id):
153153
_assert_non_empty_unicode(user_id, 'user_id')
154154

155-
user_id = urllib.parse.quote(user_id)
156-
path = f'{self.url}/v1/users/{user_id}'
155+
path = self._delete_user_url(user_id)
157156
headers = self._default_headers()
158157

159158
try:
@@ -235,48 +234,71 @@ def enroll_verified_authenticator(self, user_id, authenticator_payload, path=No
235234
except requests.exceptions.RequestException as e:
236235
raise ApiException(str(e), path) from e
237236

238-
def validate_challenge(self, token: str, user_id: Optional[str] = None) -> Dict[str, Any]:
239-
path = f"{API_CHALLENGE_URL}/validate"
237+
def validate_challenge(self, token: str, user_id: Optional[str] = None, action: Optional[str] = None) -> Dict[str, Any]:
238+
path = self._validate_challenge_url()
240239
headers = {
241240
'Content-Type': 'application/json',
242241
'Accept': 'application/json'
243242
}
244243

244+
payload = {'token': token}
245+
if user_id is not None:
246+
payload['userId'] = user_id
247+
if action is not None:
248+
payload['action'] = action
249+
245250
try:
246251
response = self.session.post(
247252
path,
248253
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
249-
data=json.dumps({'token': token, 'userId': user_id}),
254+
data=json.dumps(payload),
250255
headers=headers,
251256
timeout=self.timeout
252257
)
253258

254259
response_data = humps.decamelize(response.json())
255260

256-
action = response_data.pop('action_code', None)
257-
258-
return {'action': action, **response_data}
261+
return response_data
259262
except requests.exceptions.RequestException as e:
260263
raise ApiException(str(e), path) from e
261264

262265
def _default_headers(self):
263266
return {'Content-type': 'application/json',
264267
'Accept': '*/*',
265268
'User-Agent': self._user_agent()}
269+
266270
def _user_agent(self):
267271
return f'Authsignal Python v{self.version}'
268272

269273
def _track_url(self, user_id, action):
270-
return f'{self.url}/v1/users/{user_id}/actions/{action}'
271-
274+
path = self._ensure_versioned_path(f'/users/{user_id}/actions/{action}')
275+
return f'{self.url}{path}'
276+
272277
def _get_action_url(self, user_id, action, idempotency_key):
273-
return f'{self.url}/v1/users/{user_id}/actions/{action}/{idempotency_key}'
274-
278+
path = self._ensure_versioned_path(f'/users/{user_id}/actions/{action}/{idempotency_key}')
279+
return f'{self.url}{path}'
280+
275281
def _get_user_url(self, user_id):
276-
return f'{self.url}/v1/users/{user_id}'
282+
path = self._ensure_versioned_path(f'/users/{user_id}')
283+
return f'{self.url}{path}'
277284

278285
def _post_enrollment_url(self, user_id):
279-
return f'{self.url}/v1/users/{user_id}/authenticators'
286+
path = self._ensure_versioned_path(f'/users/{user_id}/authenticators')
287+
return f'{self.url}{path}'
288+
289+
def _validate_challenge_url(self):
290+
path = self._ensure_versioned_path(f'/validate')
291+
return f'{self.url}{path}'
292+
293+
def _delete_user_url(self, user_id):
294+
user_id = urllib.parse.quote(user_id)
295+
path = self._ensure_versioned_path(f'/users/{user_id}')
296+
return f'{self.url}{path}'
297+
298+
def _ensure_versioned_path(self, path):
299+
if not self.url.endswith(f'/{self.api_version}'):
300+
return f'/{self.api_version}{path}'
301+
return path
280302

281303
class ApiException(Exception):
282304
def __init__(self, message, url, http_status_code=None, body=None, api_status=None,
@@ -307,4 +329,3 @@ def _assert_non_empty_dict(val, name):
307329
raise TypeError('{0} must be a non-empty dict'.format(name))
308330
elif not val:
309331
raise ValueError('{0} must be a non-empty dict'.format(name))
310-

authsignal/client_tests.py

+20-8
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55

66
import client
77

8-
base_url = "https://signal.authsignal.com/v1"
9-
10-
base_challenge_url = 'https://api.authsignal.com/v1'
8+
base_url = "https://api.authsignal.com/v1"
119

1210
class Test(unittest.TestCase):
1311
def setUp(self):
@@ -133,7 +131,7 @@ def setUp(self):
133131

134132
@responses.activate
135133
def test_it_returns_success_if_user_id_is_correct(self):
136-
responses.add(responses.POST, f"{base_challenge_url}/validate",
134+
responses.add(responses.POST, f"{base_url}/validate",
137135
json={
138136
'isValid': True,
139137
'state': 'CHALLENGE_SUCCEEDED',
@@ -151,12 +149,12 @@ def test_it_returns_success_if_user_id_is_correct(self):
151149
self.assertEqual(response["state"], "CHALLENGE_SUCCEEDED")
152150
self.assertTrue(response["is_valid"])
153151

154-
155152
@responses.activate
156153
def test_delete_user_authenticator(self):
157154
self.authsignal_client = client.Client(api_key='test_api_key')
158155
user_id = 'test_user'
159156
user_authenticator_id = 'test_authenticator'
157+
160158
expected_url = f'{self.authsignal_client.url}/v1/users/{user_id}/authenticators/{user_authenticator_id}'
161159

162160
responses.add(responses.DELETE, expected_url, json={"success": True}, status=200)
@@ -170,20 +168,34 @@ def test_delete_user_authenticator(self):
170168

171169
@responses.activate
172170
def test_it_returns_success_false_if_user_id_is_incorrect(self):
173-
responses.add(responses.POST, f"{base_challenge_url}/validate",
171+
responses.add(responses.POST, f"{base_url}/validate",
174172
json={'isValid': False, 'error': 'User is invalid.'},
175173
status=400
176174
)
177175

178176
response = self.authsignal_client.validate_challenge(user_id="spoofed_id", token=self.jwt_token)
179177

180-
self.assertIsNone(response['action'])
181178
self.assertFalse(response['is_valid'])
182179
self.assertEqual(response.get("error"), "User is invalid.")
183180

181+
@responses.activate
182+
def test_it_returns_isValid_false_if_action_is_incorrect(self):
183+
responses.add(responses.POST, f"{base_url}/validate",
184+
json={
185+
'isValid': False,
186+
'error': 'Action is invalid.',
187+
},
188+
status=200
189+
)
190+
191+
response = self.authsignal_client.validate_challenge(action="malicious_action_id", token=self.jwt_token)
192+
193+
# self.assertEqual(response["error"], "CHALLENGE_SUCCEEDED")
194+
self.assertFalse(response["is_valid"])
195+
184196
@responses.activate
185197
def test_it_returns_success_true_if_no_user_id_is_provided(self):
186-
responses.add(responses.POST, f"{base_challenge_url}/validate",
198+
responses.add(responses.POST, f"{base_url}/validate",
187199
json={
188200
'isValid': True,
189201
'state': 'CHALLENGE_SUCCEEDED',

authsignal/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = '2.0.6'
1+
VERSION = '2.0.7'

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "authsignal"
3-
version = "2.0.6"
3+
version = "2.0.7"
44
description = "Authsignal Python SDK for Passwordless Step Up Authentication"
55
authors = ["justinsoong <[email protected]>"]
66
license = "MIT"

0 commit comments

Comments
 (0)