Skip to content

Commit 11403f1

Browse files
authored
Add GNP Sim Swap API (#296)
* adding the structure * add sim swap api * Bump version: 3.14.0 → 3.15.0 * blackening file
1 parent 0852476 commit 11403f1

15 files changed

+269
-4
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.14.0
2+
current_version = 3.15.0
33
commit = True
44
tag = False
55

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 3.15.0
2+
- Add support for the [Vonage Sim Swap API](https://developer.vonage.com/en/sim-swap/overview)
3+
14
# 3.14.0
25
- Add publisher-only as a valid Video API client token role
36

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
2929
- [Managing Secrets](#managing-secrets)
3030
- [Application API](#application-api)
3131
- [Users API](#users-api)
32+
- [Sim Swap API](#sim-swap-api)
3233
- [Validating Webhook Signatures](#validate-webhook-signatures)
3334
- [JWT Parameters](#jwt-parameters)
3435
- [Overriding API Attributes](#overriding-api-attributes)
@@ -1137,6 +1138,22 @@ client.users.update_user('USER_ID', params={...})
11371138
client.users.delete_user('USER_ID')
11381139
```
11391140

1141+
## Sim Swap API
1142+
1143+
This can be used to check the sim swap status of a device. You must register a business account with Vonage and create a network profile in order to use this API. [More information on authentication can be found in the Vonage Developer documentation]('https://developer.vonage.com/en/getting-started-network/authentication').
1144+
1145+
### Check the Sim Swap Status of a Number
1146+
1147+
```python
1148+
client.sim_swap.check('447700900000', max_age=24)
1149+
```
1150+
1151+
### Retrieve the Last Sim Swap Date for a Number
1152+
1153+
```python
1154+
client.sim_swap.get_last_swap_date('447700900000')
1155+
```
1156+
11401157
## Validate webhook signatures
11411158

11421159
```python

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
setup(
1111
name="vonage",
12-
version="3.14.0",
12+
version="3.15.0",
1313
description="Vonage Server SDK for Python",
1414
long_description=long_description,
1515
long_description_content_type="text/markdown",

src/vonage/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
from .client import *
22
from .ncco_builder.ncco import *
33

4-
__version__ = "3.14.0"
4+
__version__ = "3.15.0"

src/vonage/camara_auth.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
3+
4+
if TYPE_CHECKING:
5+
from vonage import Client
6+
7+
8+
class CamaraAuth:
9+
"""Class containing methods for authenticating APIs following Camara standards."""
10+
11+
def __init__(self, client: Client):
12+
self._client = client
13+
self._auth_type = 'jwt'
14+
15+
def make_oidc_request(self, number: str, scope: str):
16+
"""Make an OIDC request to authenticate a user.
17+
18+
Returns a code that can be used to request a Camara token."""
19+
20+
login_hint = f'tel:+{number}'
21+
params = {'login_hint': login_hint, 'scope': scope}
22+
23+
return self._client.post(
24+
'api-eu.vonage.com',
25+
'/oauth2/bc-authorize',
26+
params=params,
27+
auth_type=self._auth_type,
28+
body_is_json=False,
29+
)
30+
31+
def request_camara_token(
32+
self, oidc_response: dict, grant_type: str = 'urn:openid:params:grant-type:ciba'
33+
):
34+
"""Request a Camara token using an authentication request ID given as a
35+
response to the OIDC request.
36+
"""
37+
params = {
38+
'grant_type': grant_type,
39+
'auth_req_id': oidc_response['auth_req_id'],
40+
}
41+
42+
token_response = self._client.post(
43+
'api-eu.vonage.com',
44+
'/oauth2/token',
45+
params=params,
46+
auth_type=self._auth_type,
47+
body_is_json=False,
48+
)
49+
return token_response['access_token']

src/vonage/client.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .number_management import Numbers
1111
from .proactive_connect import ProactiveConnect
1212
from .redact import Redact
13+
from .sim_swap import SimSwap
1314
from .short_codes import ShortCodes
1415
from .sms import Sms
1516
from .subaccounts import Subaccounts
@@ -133,6 +134,7 @@ def __init__(
133134
self.numbers = Numbers(self)
134135
self.proactive_connect = ProactiveConnect(self)
135136
self.short_codes = ShortCodes(self)
137+
self.sim_swap = SimSwap(self)
136138
self.sms = Sms(self)
137139
self.subaccounts = Subaccounts(self)
138140
self.users = Users(self)
@@ -258,6 +260,7 @@ def post(
258260
auth_type=None,
259261
body_is_json=True,
260262
supports_signature_auth=False,
263+
oauth_token=None,
261264
):
262265
"""
263266
Low-level method to make a post request to an API server.
@@ -283,9 +286,11 @@ def post(
283286
)
284287
elif auth_type == 'header':
285288
self._request_headers['Authorization'] = self._create_header_auth_string()
289+
elif auth_type == 'oauth2':
290+
self._request_headers['Authorization'] = self._create_oauth2_auth_string(oauth_token)
286291
else:
287292
raise InvalidAuthenticationTypeError(
288-
f'Invalid authentication type. Must be one of "jwt", "header" or "params".'
293+
f'Invalid authentication type. Must be one of "jwt", "header", "params" or "oauth2".'
289294
)
290295

291296
logger.debug(
@@ -461,3 +466,6 @@ def _generate_application_jwt(self):
461466
def _create_header_auth_string(self):
462467
hash = base64.b64encode(f"{self.api_key}:{self.api_secret}".encode("utf-8")).decode("ascii")
463468
return f"Basic {hash}"
469+
470+
def _create_oauth2_auth_string(self, token: str):
471+
return f'Bearer {token}'

src/vonage/sim_swap.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
3+
4+
from .camara_auth import CamaraAuth
5+
6+
if TYPE_CHECKING:
7+
from vonage import Client
8+
9+
10+
class SimSwap:
11+
"""Class containing methods for working with the Vonage SIM Swap API."""
12+
13+
def __init__(self, client: Client):
14+
self._client = client
15+
self._auth_type = 'oauth2'
16+
self._camara_auth = CamaraAuth(client)
17+
18+
def check(self, phone_number: str, max_age: int = None):
19+
"""Check if a SIM swap has been performed in a given time frame.
20+
21+
Args:
22+
phone_number (str): The phone number to check. Use the E.164 format without a leading +.
23+
max_age (int, optional): Period in hours to be checked for SIM swap.
24+
25+
Returns:
26+
The response from the API as a dict.
27+
"""
28+
oicd_response = self._camara_auth.make_oidc_request(
29+
number=phone_number, scope='dpv:FraudPreventionAndDetection#check-sim-swap'
30+
)
31+
token = self._camara_auth.request_camara_token(oicd_response)
32+
33+
params = {'phoneNumber': phone_number}
34+
if max_age:
35+
params['maxAge'] = max_age
36+
37+
return self._client.post(
38+
'api-eu.vonage.com',
39+
'/camara/sim-swap/v040/check',
40+
params=params,
41+
auth_type=self._auth_type,
42+
oauth_token=token,
43+
)
44+
45+
def get_last_swap_date(self, phone_number: str):
46+
"""Get the last SIM swap date for a phone number.
47+
48+
Args:
49+
phone_number (str): The phone number to check. Use the E.164 format without a leading +.
50+
51+
Returns:
52+
The response from the API as a dict.
53+
"""
54+
oicd_response = self._camara_auth.make_oidc_request(
55+
number=phone_number,
56+
scope='dpv:FraudPreventionAndDetection#retrieve-sim-swap-date',
57+
)
58+
token = self._camara_auth.request_camara_token(oicd_response)
59+
return self._client.post(
60+
'api-eu.vonage.com',
61+
'/camara/sim-swap/v040/retrieve-date',
62+
params={'phoneNumber': phone_number},
63+
auth_type=self._auth_type,
64+
oauth_token=token,
65+
)

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,17 @@ def proc(client):
139139
import vonage
140140

141141
return vonage.ProactiveConnect(client)
142+
143+
144+
@pytest.fixture
145+
def camara_auth(client):
146+
from vonage.camara_auth import CamaraAuth
147+
148+
return CamaraAuth(client)
149+
150+
151+
@pytest.fixture
152+
def sim_swap(client):
153+
import vonage
154+
155+
return vonage.SimSwap(client)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"auth_req_id": "arid/8b0d35f3-4627-487c-a776-aegtdsf4rsd2",
3+
"expires_in": 300,
4+
"interval": 0
5+
}

0 commit comments

Comments
 (0)