Skip to content

Commit

Permalink
Add Number Verification API (#298)
Browse files Browse the repository at this point in the history
* add number verification api

* adjust for encodings

* Bump version: 3.15.0 → 3.16.0
  • Loading branch information
maxkahan authored Jun 7, 2024
1 parent 11403f1 commit 2b8d988
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 3.15.0
current_version = 3.16.0
commit = True
tag = False

Expand Down
3 changes: 3 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 3.16.0
- Add support for the [Vonage Number Verification API](https://developer.vonage.com/number-verification/overview)

# 3.15.0
- Add support for the [Vonage Sim Swap API](https://developer.vonage.com/en/sim-swap/overview)

Expand Down
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
- [Application API](#application-api)
- [Users API](#users-api)
- [Sim Swap API](#sim-swap-api)
- [Number Verification API](#number-verification-api)
- [Validating Webhook Signatures](#validate-webhook-signatures)
- [JWT Parameters](#jwt-parameters)
- [Overriding API Attributes](#overriding-api-attributes)
Expand Down Expand Up @@ -1154,7 +1155,41 @@ client.sim_swap.check('447700900000', max_age=24)
client.sim_swap.get_last_swap_date('447700900000')
```

## Validate webhook signatures
## Number Verification API

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').

### Get an OIDC URL

Get an OIDC URL for use in your front-end application.

```python
url = client.number_verification.get_oidc_url(
redirect_uri='https://example.com/callback',
state='state_id',
login_hint='447700900000',
)
print(url)
```

### Create an Access Token from an Authentication Code

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.

```python
access_token = client.number_verification.create_camara_token('code', 'https://example.com/callback')
```

You can then use this access token when making a Number Verification request.

### Make a Number Verification Request

```python
response = client.number_verification.verify(access_token, phone_number='447700900000')
print(response)
```

## Validate Webhook Signatures

```python
client = vonage.Client(signature_secret='secret')
Expand Down
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
-e .
pytest==7.4.2
responses==0.22.0
pytest==8.2.2
responses==0.25.0
pydantic==2.7.3
coverage
pydantic==2.5.2

bump2version
build
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

setup(
name="vonage",
version="3.15.0",
version="3.16.0",
description="Vonage Server SDK for Python",
long_description=long_description,
long_description_content_type="text/markdown",
Expand Down
2 changes: 1 addition & 1 deletion src/vonage/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .client import *
from .ncco_builder.ncco import *

__version__ = "3.15.0"
__version__ = "3.16.0"
52 changes: 51 additions & 1 deletion src/vonage/camara_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from urllib.parse import urlencode, urlunparse


if TYPE_CHECKING:
from vonage import Client
Expand Down Expand Up @@ -28,7 +30,44 @@ def make_oidc_request(self, number: str, scope: str):
body_is_json=False,
)

def request_camara_token(
def get_oidc_url(
self,
redirect_uri: str,
state: str = None,
login_hint: str = None,
scope: str = 'openid dpv:FraudPreventionAndDetection#number-verification-verify-read',
):
"""Get the URL to use for authentication in a front-end application.
Args:
redirect_uri (str): The URI to redirect to after authentication.
scope (str): The scope of the request.
state (str): A unique identifier for the request. Can be any string.
login_hint (str): The phone number to use for the request.
Returns:
The URL to use to make an OIDC request in a front-end application.
"""
base_url = 'https://oidc.idp.vonage.com/oauth2/auth'

params = {
'client_id': self._client.application_id,
'redirect_uri': redirect_uri,
'response_type': 'code',
'scope': scope,
}
if state:
params['state'] = state
if login_hint:
if login_hint.startswith('+'):
params['login_hint'] = login_hint
else:
params['login_hint'] = f'+{login_hint}'

full_url = urlunparse(('', '', base_url, '', urlencode(params), ''))
return full_url

def request_backend_camara_token(
self, oidc_response: dict, grant_type: str = 'urn:openid:params:grant-type:ciba'
):
"""Request a Camara token using an authentication request ID given as a
Expand All @@ -38,7 +77,18 @@ def request_camara_token(
'grant_type': grant_type,
'auth_req_id': oidc_response['auth_req_id'],
}
return self._request_camara_token(params)

def request_frontend_camara_token(self, code: str, redirect_uri: str):
"""Request a Camara token using a code from an OIDC response."""
params = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': redirect_uri,
}
return self._request_camara_token(params)

def _request_camara_token(self, params: dict):
token_response = self._client.post(
'api-eu.vonage.com',
'/oauth2/token',
Expand Down
2 changes: 2 additions & 0 deletions src/vonage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .messages import Messages
from .number_insight import NumberInsight
from .number_management import Numbers
from .number_verification import NumberVerification
from .proactive_connect import ProactiveConnect
from .redact import Redact
from .sim_swap import SimSwap
Expand Down Expand Up @@ -132,6 +133,7 @@ def __init__(
self.messages = Messages(self)
self.number_insight = NumberInsight(self)
self.numbers = Numbers(self)
self.number_verification = NumberVerification(self)
self.proactive_connect = ProactiveConnect(self)
self.short_codes = ShortCodes(self)
self.sim_swap = SimSwap(self)
Expand Down
4 changes: 4 additions & 0 deletions src/vonage/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ class TokenExpiryError(ClientError):

class SipError(ClientError):
"""Error related to usage of SIP calls."""


class NumberVerificationError(ClientError):
"""An error relating to the Number Verification API."""
67 changes: 67 additions & 0 deletions src/vonage/number_verification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from __future__ import annotations
from typing import TYPE_CHECKING

from vonage.errors import NumberVerificationError

from .camara_auth import CamaraAuth

if TYPE_CHECKING:
from vonage import Client


class NumberVerification:
"""Class containing methods for working with the Vonage Number Verification API."""

def __init__(self, client: Client):
self._client = client
self._auth_type = 'oauth2'
self._camara_auth = CamaraAuth(client)
self._nvtoken = None

def get_oidc_url(
self,
redirect_uri: str,
state: str = None,
login_hint: str = None,
scope: str = 'openid dpv:FraudPreventionAndDetection#number-verification-verify-read',
):
"""Get the URL to use for authentication in a front-end application.
Args:
redirect_uri (str): The URI to redirect to after authentication.
scope (str): The scope of the request.
state (str): A unique identifier for the request. Can be any string.
login_hint (str): The phone number to use for the request.
Returns:
The URL to use to make an OIDC request in a front-end application.
"""
return self._camara_auth.get_oidc_url(
redirect_uri=redirect_uri,
scope=scope,
state=state,
login_hint=login_hint,
)

def exchange_code_for_token(self, code: str, redirect_uri: str) -> str:
return self._camara_auth.request_frontend_camara_token(code, redirect_uri)

def verify(self, access_token: str, phone_number: str = None, hashed_phone_number: str = None):
"""Verify a phone number using the Number Verification API."""

if phone_number and hashed_phone_number:
raise NumberVerificationError(
'Only one of "phone_number" and "hashed_phone_number" can be provided.'
)
if phone_number:
params = {'phoneNumber': phone_number}
elif hashed_phone_number:
params = {'hashedPhoneNumber': hashed_phone_number}

return self._client.post(
'api-eu.vonage.com',
'/camara/number-verification/v031/verify',
params=params,
auth_type=self._auth_type,
oauth_token=access_token,
)
4 changes: 2 additions & 2 deletions src/vonage/sim_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def check(self, phone_number: str, max_age: int = None):
oicd_response = self._camara_auth.make_oidc_request(
number=phone_number, scope='dpv:FraudPreventionAndDetection#check-sim-swap'
)
token = self._camara_auth.request_camara_token(oicd_response)
token = self._camara_auth.request_backend_camara_token(oicd_response)

params = {'phoneNumber': phone_number}
if max_age:
Expand All @@ -55,7 +55,7 @@ def get_last_swap_date(self, phone_number: str):
number=phone_number,
scope='dpv:FraudPreventionAndDetection#retrieve-sim-swap-date',
)
token = self._camara_auth.request_camara_token(oicd_response)
token = self._camara_auth.request_backend_camara_token(oicd_response)
return self._client.post(
'api-eu.vonage.com',
'/camara/sim-swap/v040/retrieve-date',
Expand Down
Loading

0 comments on commit 2b8d988

Please sign in to comment.