Skip to content

Commit 2b8d988

Browse files
authored
Add Number Verification API (#298)
* add number verification api * adjust for encodings * Bump version: 3.15.0 → 3.16.0
1 parent 11403f1 commit 2b8d988

15 files changed

+306
-44
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.15.0
2+
current_version = 3.16.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.16.0
2+
- Add support for the [Vonage Number Verification API](https://developer.vonage.com/number-verification/overview)
3+
14
# 3.15.0
25
- Add support for the [Vonage Sim Swap API](https://developer.vonage.com/en/sim-swap/overview)
36

README.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
3030
- [Application API](#application-api)
3131
- [Users API](#users-api)
3232
- [Sim Swap API](#sim-swap-api)
33+
- [Number Verification API](#number-verification-api)
3334
- [Validating Webhook Signatures](#validate-webhook-signatures)
3435
- [JWT Parameters](#jwt-parameters)
3536
- [Overriding API Attributes](#overriding-api-attributes)
@@ -1154,7 +1155,41 @@ client.sim_swap.check('447700900000', max_age=24)
11541155
client.sim_swap.get_last_swap_date('447700900000')
11551156
```
11561157

1157-
## Validate webhook signatures
1158+
## Number Verification API
1159+
1160+
This can be used to verify a mobile 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').
1161+
1162+
### Get an OIDC URL
1163+
1164+
Get an OIDC URL for use in your front-end application.
1165+
1166+
```python
1167+
url = client.number_verification.get_oidc_url(
1168+
redirect_uri='https://example.com/callback',
1169+
state='state_id',
1170+
login_hint='447700900000',
1171+
)
1172+
print(url)
1173+
```
1174+
1175+
### Create an Access Token from an Authentication Code
1176+
1177+
To verify a number, you need a Camara access token. Your front-end application should have made an OIDC request that returned a `code`. Use this with your `redirect_uri` to generate an access token.
1178+
1179+
```python
1180+
access_token = client.number_verification.create_camara_token('code', 'https://example.com/callback')
1181+
```
1182+
1183+
You can then use this access token when making a Number Verification request.
1184+
1185+
### Make a Number Verification Request
1186+
1187+
```python
1188+
response = client.number_verification.verify(access_token, phone_number='447700900000')
1189+
print(response)
1190+
```
1191+
1192+
## Validate Webhook Signatures
11581193

11591194
```python
11601195
client = vonage.Client(signature_secret='secret')

requirements.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
-e .
2-
pytest==7.4.2
3-
responses==0.22.0
2+
pytest==8.2.2
3+
responses==0.25.0
4+
pydantic==2.7.3
45
coverage
5-
pydantic==2.5.2
66

77
bump2version
88
build

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.15.0",
12+
version="3.16.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.15.0"
4+
__version__ = "3.16.0"

src/vonage/camara_auth.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22
from typing import TYPE_CHECKING
3+
from urllib.parse import urlencode, urlunparse
4+
35

46
if TYPE_CHECKING:
57
from vonage import Client
@@ -28,7 +30,44 @@ def make_oidc_request(self, number: str, scope: str):
2830
body_is_json=False,
2931
)
3032

31-
def request_camara_token(
33+
def get_oidc_url(
34+
self,
35+
redirect_uri: str,
36+
state: str = None,
37+
login_hint: str = None,
38+
scope: str = 'openid dpv:FraudPreventionAndDetection#number-verification-verify-read',
39+
):
40+
"""Get the URL to use for authentication in a front-end application.
41+
42+
Args:
43+
redirect_uri (str): The URI to redirect to after authentication.
44+
scope (str): The scope of the request.
45+
state (str): A unique identifier for the request. Can be any string.
46+
login_hint (str): The phone number to use for the request.
47+
48+
Returns:
49+
The URL to use to make an OIDC request in a front-end application.
50+
"""
51+
base_url = 'https://oidc.idp.vonage.com/oauth2/auth'
52+
53+
params = {
54+
'client_id': self._client.application_id,
55+
'redirect_uri': redirect_uri,
56+
'response_type': 'code',
57+
'scope': scope,
58+
}
59+
if state:
60+
params['state'] = state
61+
if login_hint:
62+
if login_hint.startswith('+'):
63+
params['login_hint'] = login_hint
64+
else:
65+
params['login_hint'] = f'+{login_hint}'
66+
67+
full_url = urlunparse(('', '', base_url, '', urlencode(params), ''))
68+
return full_url
69+
70+
def request_backend_camara_token(
3271
self, oidc_response: dict, grant_type: str = 'urn:openid:params:grant-type:ciba'
3372
):
3473
"""Request a Camara token using an authentication request ID given as a
@@ -38,7 +77,18 @@ def request_camara_token(
3877
'grant_type': grant_type,
3978
'auth_req_id': oidc_response['auth_req_id'],
4079
}
80+
return self._request_camara_token(params)
81+
82+
def request_frontend_camara_token(self, code: str, redirect_uri: str):
83+
"""Request a Camara token using a code from an OIDC response."""
84+
params = {
85+
'grant_type': 'authorization_code',
86+
'code': code,
87+
'redirect_uri': redirect_uri,
88+
}
89+
return self._request_camara_token(params)
4190

91+
def _request_camara_token(self, params: dict):
4292
token_response = self._client.post(
4393
'api-eu.vonage.com',
4494
'/oauth2/token',

src/vonage/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .messages import Messages
99
from .number_insight import NumberInsight
1010
from .number_management import Numbers
11+
from .number_verification import NumberVerification
1112
from .proactive_connect import ProactiveConnect
1213
from .redact import Redact
1314
from .sim_swap import SimSwap
@@ -132,6 +133,7 @@ def __init__(
132133
self.messages = Messages(self)
133134
self.number_insight = NumberInsight(self)
134135
self.numbers = Numbers(self)
136+
self.number_verification = NumberVerification(self)
135137
self.proactive_connect = ProactiveConnect(self)
136138
self.short_codes = ShortCodes(self)
137139
self.sim_swap = SimSwap(self)

src/vonage/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,7 @@ class TokenExpiryError(ClientError):
7070

7171
class SipError(ClientError):
7272
"""Error related to usage of SIP calls."""
73+
74+
75+
class NumberVerificationError(ClientError):
76+
"""An error relating to the Number Verification API."""

src/vonage/number_verification.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
from typing import TYPE_CHECKING
3+
4+
from vonage.errors import NumberVerificationError
5+
6+
from .camara_auth import CamaraAuth
7+
8+
if TYPE_CHECKING:
9+
from vonage import Client
10+
11+
12+
class NumberVerification:
13+
"""Class containing methods for working with the Vonage Number Verification API."""
14+
15+
def __init__(self, client: Client):
16+
self._client = client
17+
self._auth_type = 'oauth2'
18+
self._camara_auth = CamaraAuth(client)
19+
self._nvtoken = None
20+
21+
def get_oidc_url(
22+
self,
23+
redirect_uri: str,
24+
state: str = None,
25+
login_hint: str = None,
26+
scope: str = 'openid dpv:FraudPreventionAndDetection#number-verification-verify-read',
27+
):
28+
"""Get the URL to use for authentication in a front-end application.
29+
30+
Args:
31+
redirect_uri (str): The URI to redirect to after authentication.
32+
scope (str): The scope of the request.
33+
state (str): A unique identifier for the request. Can be any string.
34+
login_hint (str): The phone number to use for the request.
35+
36+
Returns:
37+
The URL to use to make an OIDC request in a front-end application.
38+
"""
39+
return self._camara_auth.get_oidc_url(
40+
redirect_uri=redirect_uri,
41+
scope=scope,
42+
state=state,
43+
login_hint=login_hint,
44+
)
45+
46+
def exchange_code_for_token(self, code: str, redirect_uri: str) -> str:
47+
return self._camara_auth.request_frontend_camara_token(code, redirect_uri)
48+
49+
def verify(self, access_token: str, phone_number: str = None, hashed_phone_number: str = None):
50+
"""Verify a phone number using the Number Verification API."""
51+
52+
if phone_number and hashed_phone_number:
53+
raise NumberVerificationError(
54+
'Only one of "phone_number" and "hashed_phone_number" can be provided.'
55+
)
56+
if phone_number:
57+
params = {'phoneNumber': phone_number}
58+
elif hashed_phone_number:
59+
params = {'hashedPhoneNumber': hashed_phone_number}
60+
61+
return self._client.post(
62+
'api-eu.vonage.com',
63+
'/camara/number-verification/v031/verify',
64+
params=params,
65+
auth_type=self._auth_type,
66+
oauth_token=access_token,
67+
)

0 commit comments

Comments
 (0)