Skip to content

Commit

Permalink
Add verify2 (Vonage#254)
Browse files Browse the repository at this point in the history
* starting verify implementation

* using Literal from typing-extensions for 3.7 compatibility

* add new_request and check_code verify2 methods, use new class structure, add tests

* adding more test cases

* adding custom verification code validation and testing

* moving custom code validation from workflow object to the main request body

* adding fraud_check parameter to verify v2 request

* adding verify v2 to readme, adding verify2.cancel_verification

* removing build on PR as we already build on push

* fixing typo in link
  • Loading branch information
maxkahan authored May 16, 2023
1 parent 99b9635 commit b3b5d34
Show file tree
Hide file tree
Showing 21 changed files with 740 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Build
on: [push, pull_request]
on: [push]
jobs:
test:
name: Test
Expand Down
72 changes: 70 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ need a Vonage account. Sign up [for free at vonage.com][signup].
- [Messages API](#messages-api)
- [Voice API](#voice-api)
- [NCCO Builder](#ncco-builder)
- [Verify API](#verify-api)
- [Verify V2 API](#verify-v2-api)
- [Verify V1 API](#verify-v1-api)
- [Number Insight API](#number-insight-api)
- [Account API](#account-api)
- [Number Management API](#number-management-api)
Expand Down Expand Up @@ -406,8 +407,75 @@ pprint(response)

When using the `connect` action, use the parameter `from_` to specify the recipient (as `from` is a reserved keyword in Python!)

## Verify V2 API

## Verify API
V2 of the Vonage Verify API lets you send verification codes via SMS, WhatsApp, Voice and Email

You can also verify a user by WhatsApp Interactive Message or by Silent Authentication on their mobile device.

### Send a verification code

```python
params = {
'brand': 'ACME, Inc',
'workflow': [{'channel': 'sms', 'to': '447700900000'}]
}
verify_request = verify2.new_request(params)
```

### Use silent authentication, with email as a fallback

```python
params = {
'brand': 'ACME, Inc',
'workflow': [
{'channel': 'silent_auth', 'to': '447700900000'},
{'channel': 'email', 'to': 'customer@example.com', 'from': 'business@example.com'}
]
}
verify_request = verify2.new_request(params)
```

### Send a verification code with custom options, including a custom code

```python
params = {
'locale': 'en-gb',
'channel_timeout': 120,
'client_ref': 'my client reference',
'code': 'asdf1234',
'brand': 'ACME, Inc',
'workflow': [{'channel': 'sms', 'to': '447700900000', 'app_hash': 'asdfghjklqw'}],
}
verify_request = verify2.new_request(params)
```

### Send a verification request to a blocked network

This feature is only enabled if you have requested for it to be added to your account.

```python
params = {
'brand': 'ACME, Inc',
'fraud_check': False,
'workflow': [{'channel': 'sms', 'to': '447700900000'}]
}
verify_request = verify2.new_request(params)
```

### Check a verification code

```python
verify2.check_code(REQUEST_ID, CODE)
```

### Cancel an ongoing verification

```python
verify2.cancel_verification(REQUEST_ID)
```

## Verify V1 API

### Search for a Verification request

Expand Down
11 changes: 9 additions & 2 deletions src/vonage/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from .ussd import Ussd
from .voice import Voice
from .verify import Verify
from .verify2 import Verify2

import logging
from platform import python_version
Expand Down Expand Up @@ -123,6 +124,7 @@ def __init__(
self.sms = Sms(self)
self.ussd = Ussd(self)
self.verify = Verify(self)
self.verify2 = Verify2(self)
self.voice = Voice(self)

self.timeout = timeout
Expand Down Expand Up @@ -278,7 +280,11 @@ def parse(self, host, response: Response):
return None
elif 200 <= response.status_code < 300:
# Strip off any encoding from the content-type header:
content_mime = response.headers.get("content-type").split(";", 1)[0]
try:
content_mime = response.headers.get("content-type").split(";", 1)[0]
except AttributeError:
if response.json() is None:
return None
if content_mime == "application/json":
return response.json()
else:
Expand All @@ -295,7 +301,8 @@ def parse(self, host, response: Response):
detail = error_data["detail"]
type = error_data["type"]
message = f"{title}: {detail} ({type})"

else:
message = error_data
except JSONDecodeError:
pass
raise ClientError(message)
Expand Down
8 changes: 7 additions & 1 deletion src/vonage/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,14 @@ class MessagesError(Error):
class PricingTypeError(Error):
"""A pricing type was specified that is not allowed."""


class RedactError(Error):
"""Error related to the Redact class or Redact API."""


class InvalidAuthenticationTypeError(Error):
"""An authentication method was specified that is not allowed"""
"""An authentication method was specified that is not allowed."""


class Verify2Error(ClientError):
"""An error relating to the Verify (V2) API."""
10 changes: 6 additions & 4 deletions src/vonage/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ def __init__(self, client):

def start_verification(self, params=None, **kwargs):
return self._client.post(
self._client.api_host(),
"/verify/json",
self._client.api_host(),
"/verify/json",
params or kwargs,
**Verify.defaults,
)
Expand Down Expand Up @@ -44,6 +44,8 @@ def trigger_next_event(self, request_id):

def psd2(self, params=None, **kwargs):
return self._client.post(
self._client.api_host(), "/verify/psd2/json", params or kwargs, **Verify.defaults,
self._client.api_host(),
"/verify/psd2/json",
params or kwargs,
**Verify.defaults,
)

107 changes: 107 additions & 0 deletions src/vonage/verify2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from pydantic import BaseModel, ValidationError, validator, conint, constr
from typing import Optional, List

import copy
import re

from .errors import Verify2Error


class Verify2:
valid_channels = [
'sms',
'whatsapp',
'whatsapp_interactive',
'voice',
'email',
'silent_auth',
]

def __init__(self, client):
self._client = client
self._auth_type = 'jwt'

def new_request(self, params: dict):
try:
params_to_verify = copy.deepcopy(params)
Verify2.VerifyRequest.parse_obj(params_to_verify)
except (ValidationError, Verify2Error) as err:
raise err

if not hasattr(self._client, '_application_id'):
self._auth_type = 'header'

return self._client.post(
self._client.api_host(),
'/v2/verify',
params,
auth_type=self._auth_type,
)

def check_code(self, request_id: str, code: str):
params = {'code': str(code)}

if not hasattr(self._client, '_application_id'):
self._auth_type = 'header'

return self._client.post(
self._client.api_host(),
f'/v2/verify/{request_id}',
params,
auth_type=self._auth_type,
)

def cancel_verification(self, request_id: str):
if not hasattr(self._client, '_application_id'):
self._auth_type = 'header'

return self._client.delete(
self._client.api_host(),
f'/v2/verify/{request_id}',
auth_type=self._auth_type,
)

class VerifyRequest(BaseModel):
brand: str
workflow: List[dict]
locale: Optional[str]
channel_timeout: Optional[conint(ge=60, le=900)]
client_ref: Optional[str]
code_length: Optional[conint(ge=4, le=10)]
fraud_check: Optional[bool]
code: Optional[constr(min_length=4, max_length=10, regex='^(?=[a-zA-Z0-9]{4,10}$)[a-zA-Z0-9]*$')]

@validator('workflow')
def check_valid_workflow(cls, v):
for workflow in v:
Verify2._check_valid_channel(workflow)
Verify2._check_valid_recipient(workflow)
Verify2._check_app_hash(workflow)
if workflow['channel'] == 'whatsapp' and 'from' in workflow:
Verify2._check_whatsapp_sender(workflow)

def _check_valid_channel(workflow):
if 'channel' not in workflow or workflow['channel'] not in Verify2.valid_channels:
raise Verify2Error(
f'You must specify a valid verify channel inside the "workflow" object, one of: "{Verify2.valid_channels}"'
)

def _check_valid_recipient(workflow):
if 'to' not in workflow or (
workflow['channel'] != 'email' and not re.search(r'^[1-9]\d{6,14}$', workflow['to'])
):
raise Verify2Error(f'You must specify a valid "to" value for channel "{workflow["channel"]}"')

def _check_app_hash(workflow):
if workflow['channel'] == 'sms' and 'app_hash' in workflow:
if type(workflow['app_hash']) != str or len(workflow['app_hash']) != 11:
raise Verify2Error(
'Invalid "app_hash" specified. If specifying app_hash, \
it must be passed as a string and contain exactly 11 characters.'
)
elif workflow['channel'] != 'sms' and 'app_hash' in workflow:
raise Verify2Error('Cannot specify a value for "app_hash" unless using SMS for authentication.')

def _check_whatsapp_sender(workflow):
if not re.search(r'^[1-9]\d{6,14}$', workflow['from']):
raise Verify2Error(f'You must specify a valid "from" value if included.')
16 changes: 12 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,50 +69,58 @@ def verify(client):

return vonage.Verify(client)


@pytest.fixture
def number_insight(client):
import vonage

return vonage.NumberInsight(client)


@pytest.fixture
def account(client):
import vonage

return vonage.Account(client)


@pytest.fixture
def numbers(client):
import vonage

return vonage.Numbers(client)


@pytest.fixture
def ussd(client):
import vonage

return vonage.Ussd(client)


@pytest.fixture
def short_codes(client):
import vonage

return vonage.ShortCodes(client)


@pytest.fixture
def messages(client):
import vonage

return vonage.Messages(client)


@pytest.fixture
def redact(client):
import vonage

return vonage.Redact(client)


@pytest.fixture
def application_v2(client):
import vonage

return vonage.ApplicationV2(client)
return vonage.ApplicationV2(client)
Empty file added tests/data/no_content.json
Empty file.
6 changes: 6 additions & 0 deletions tests/data/verify2/already_verified.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "https://developer.nexmo.com/api-errors#not-found",
"title": "Not Found",
"detail": "Request '5fcc26ef-1e54-48a6-83ab-c47546a19824' was not found or it has been verified already.",
"instance": "02cabfcc-2e09-4b5d-b098-1fa7ccef4607"
}
4 changes: 4 additions & 0 deletions tests/data/verify2/check_code.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"request_id": "e043d872-459b-4750-a20c-d33f91d6959f",
"status": "completed"
}
6 changes: 6 additions & 0 deletions tests/data/verify2/code_not_supported.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"title": "Conflict",
"detail": "The current Verify workflow step does not support a code.",
"instance": "690c48de-c5d1-49f2-8712-b3b0a840f911",
"type": "https://developer.nexmo.com/api-errors#conflict"
}
3 changes: 3 additions & 0 deletions tests/data/verify2/create_request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"request_id": "c11236f4-00bf-4b89-84ba-88b25df97315"
}
7 changes: 7 additions & 0 deletions tests/data/verify2/error_conflict.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"title": "Conflict",
"type": "https://www.developer.vonage.com/api-errors/verify#conflict",
"detail": "Concurrent verifications to the same number are not allowed.",
"instance": "738f9313-418a-4259-9b0d-6670f06fa82d",
"request_id": "575a2054-aaaf-4405-994e-290be7b9a91f"
}
6 changes: 6 additions & 0 deletions tests/data/verify2/fraud_check_invalid_account.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "https://developer.nexmo.com/api-errors#forbidden",
"title": "Forbidden",
"detail": "Your account does not have permission to perform this action.",
"instance": "1995bc0d-c850-4bf0-aa1e-6c40da43d3bf"
}
6 changes: 6 additions & 0 deletions tests/data/verify2/invalid_code.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "https://developer.nexmo.com/api-errors#bad-request",
"title": "Invalid Code",
"detail": "The code you provided does not match the expected value.",
"instance": "16d6bca6-c0dc-4add-94b2-0dbc12cba83b"
}
Loading

0 comments on commit b3b5d34

Please sign in to comment.