Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Number Verification API #298

Merged
merged 3 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading