diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9506b63c..8fbf3400 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -22,11 +22,11 @@ jobs: with: python-version: ${{ matrix.python-version }} - - run: pip install -U setuptools + - run: pip install -U setuptools wheel - - run: pip install -r requirements.txt + - run: pip install --no-cache-dir -r requirements.txt - - run: pip install -e .[dev] + - run: pip install --no-cache-dir -e .[dev] - run: pytest -v @@ -44,12 +44,12 @@ jobs: with: python-version: 3.9 - - run: pip install --upgrade setuptools + - run: pip install --upgrade setuptools wheel - - run: pushd examples/aml && pip install -r requirements.txt && popd + - run: pushd examples/aml && pip install --no-cache-dir -r requirements.txt && popd - - run: pushd examples/yoti_example_django && pip install --upgrade pip && pip install -r requirements.txt && popd + - run: pushd examples/yoti_example_django && pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt && popd - - run: pushd examples/yoti_example_flask && pip install -r requirements.txt && popd + - run: pushd examples/yoti_example_flask && pip install --no-cache-dir -r requirements.txt && popd - - run: pushd examples/doc_scan && pip install -r requirements.txt && popd + - run: pushd examples/doc_scan && pip install --no-cache-dir -r requirements.txt && popd diff --git a/examples/digitalidentity/.env.example b/examples/digitalidentity/.env.example new file mode 100644 index 00000000..950e65a9 --- /dev/null +++ b/examples/digitalidentity/.env.example @@ -0,0 +1,3 @@ +# Required Keys +YOTI_CLIENT_SDK_ID=yourClientSdkId +YOTI_KEY_FILE_PATH=yourKeyFilePath diff --git a/examples/digitalidentity/README.md b/examples/digitalidentity/README.md new file mode 100644 index 00000000..2bfa2f8d --- /dev/null +++ b/examples/digitalidentity/README.md @@ -0,0 +1,11 @@ +# Yoti Share v2 Python Example + +## Running the example project + +1. Rename the [.env.example](.env.example) file to `.env` and fill in the required configuration values +2. Add your SDK ID to the [templates/index.html](templates/index.html) file +3. Create a virtual environment with `python3 -m venv .venv` +4. Active the environment `. .venv/bin/activate` +5. Install the dependencies with `pip install -r requirements.txt` +6. Start the server `flask run --port 8000` +7. Visit `http://127.0.0.1:8000` diff --git a/examples/digitalidentity/__init__.py b/examples/digitalidentity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/digitalidentity/app.py b/examples/digitalidentity/app.py new file mode 100644 index 00000000..515c5ddd --- /dev/null +++ b/examples/digitalidentity/app.py @@ -0,0 +1,134 @@ +import json, requests +from flask import Flask, Response, request, render_template + + +from cryptography.fernet import base64 + +from settings import YOTI_API_URL, YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH + +from yoti_python_sdk.digital_identity import ( + DigitalIdentityClient +) + +app = Flask(__name__) + +@app.route("/") +def index(): + return render_template("index.html") + +@app.route("/sessions") +def sessions(): + session_config = { + "policy": { + "wanted": [ + { + "name": "date_of_birth", + "derivation": "age_over:18", + "optional": "false" + } + , + { + "name": "full_name" + }, + { + "name": "email_address" + }, + { + "name": "phone_number" + }, + { + "name": "selfie" + }, + { + "name": "date_of_birth", + "derivation": "age_over:18" + }, + { + "name": "nationality" + }, + { + "name": "gender" + }, + { + "name": "document_details" + }, + { + "name": "document_images" + }], + "wanted_auth_types": [], + "wanted_remember_me": "false", + }, + "extensions": [], + "subject": { + "subject_id": "some_subject_id_string" + }, # Optional reference to a user ID + "notification": { + "url": "https://webhook.site/818dc66b-e18b-4767-92c5-47c7af21629c", + "method": "POST", + "headers": {}, + "verifyTls": "true" + }, + "redirectUri": "/profile" # Mandatory redirect URI but not required for Non-browser flows + } + + digital_identity_client = DigitalIdentityClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH, YOTI_API_URL) + + share_session_result = digital_identity_client.create_share_session(session_config) + + session_id = share_session_result.id + + # create_qr_code_result = create_share_qr_code(share_session_result.id) + # get_share_session_result = get_share_session(share_session_result.id) + # get_qr_code_result = get_share_qr_code(create_qr_code_result.id) + + # Return Session ID JSON + return json.dumps({"session_id": session_id}) + +@app.route("/create-qr-code") +def create_qr_code(): + # Get query params - sessionId + session_id = request.args.get('sessionId') + digital_identity_client = DigitalIdentityClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH, YOTI_API_URL) + +# Create QR Code + create_qr_code_result = digital_identity_client.create_share_qr_code(session_id) + + # Return QR Code ID and URI JSON + return json.dumps({"qr_code_id": create_qr_code_result.id, "qr_code_uri": create_qr_code_result.uri}) + +@app.route("/render-qr-code") +def render_qr_code(): + # Get query params - qrCodeUri + qr_code_uri = request.args.get('qrCodeUri') + # Make a POST request to the API to create a QR Code image + url = "https://api.yoti.com/api/v1/qrcodes/image" + payload = { "url": str(qr_code_uri) } + headers = { + "Accept": "image/png", + "Content-Type": "application/json" + } + + response = requests.request("POST", url, json=payload, headers=headers) + + # Return QR Code Image as PNG + return Response(response.content, mimetype='image/png') + +@app.route("/profile") +def profile(): + # Get query params - receiptId + receipt_id = request.args.get('receiptId') + digital_identity_client = DigitalIdentityClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH, YOTI_API_URL) + + share_receipt = digital_identity_client.get_share_receipt(receipt_id) + age_over_verification = share_receipt.userContent.profile.find_age_over_verification(18) + selfie = share_receipt.userContent.profile.selfie.value + attribute_list = share_receipt.userContent.profile.attributes + full_name = share_receipt.userContent.profile.full_name.value + data = base64.b64encode(selfie).decode("utf-8") + selfie_image = "data:{0};base64,{1}".format("image/jpeg", data) + age_verified = age_over_verification.result + + return render_template("profile.html", age_verified=age_verified, selfie=selfie, full_name=full_name, selfie_image=selfie_image, attribute_list = attribute_list) + +if __name__ == "__main__": + app.run(host="0.0.0.0", ssl_context="adhoc") diff --git a/examples/digitalidentity/keys/Mehmet Prod SDK application-access-security.pem b/examples/digitalidentity/keys/Mehmet Prod SDK application-access-security.pem new file mode 100644 index 00000000..c3fc779a --- /dev/null +++ b/examples/digitalidentity/keys/Mehmet Prod SDK application-access-security.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAxtXhHY0ZxiTWSGjC8Y0EHsXpmV3FPJo6Dne1skP9e7pe2i0I +5tmvPVcQsOckrBjVP/H4G+Lzqtu7JfbD5BHZeqrec6PEITGBsrTAhcLjVQzUefN/ +mFkOinlOXBuPxMP5hZttFrNjqPjRhhlYrtQV2uOh/F/UGpqupgtJPKgF6mquDEGA +lHAxCDMwwl+NtZgg1N5w7pDUJYc/cNwI8X0afTTLneCqTSD+t9dARVgcXpnJm1WK +0XyJ4u6ENgN1fUcKPC3MP2isbTZe895dAVlvUH2ADD8qYfP8Nz8tcQ3DbXfjJC73 +hvuawmZIzotHqj2p6Oh9E6F3nLwlQPE18a599QIDAQABAoIBAArWI710T/QKVGpg +WUWIYbHap/NNlr8JicH5mLu1NGauntZFr49DTGdrrBN0GX3OoaqpQZQlf5GvhYjZ +ZM40gdWLY/HJ+lmzxMWMT9TKbRDY0OivkmPncKEv4MsozmJTKvFy6dRbpQITw3mL +PpfSo7lJAC5Mu7bSeNPAWDa30pC2tHtxjtoAstdABgqDMLpwD19O3FkWowR5q+6l +hFHi+bk+pzMc2X51PB1zsbpC+7US5p+N+qeo3ebrnncmj3yvh5OSah1NY+2CDWfR +gLyu0LrrGdgBg2lpAHdQvvQJhnQzVArKhItwVmW1TIcze0oNsZV/BOJetLj4lctz +Ih4xUjECgYEA+ZHJLsOMFmtFKWEOgDMrkRkfLpbgf0HuPM17InDLPDTaUeGjyTdM +CSxQiqbIW6g3bzHZ5idRhVYIPMgZ7/71X3Nueif5nNG5PHhiuNq7o1R2iuJ6FKms +p+/G9NsYG7THm2QlanFqp87hyuMkSt08Ugicb1GaOLs7UAcuCfnnF2UCgYEAy/Vw +5ezN5XiC7XxI/XszQRAuBPnHlQLMSAyUvZ6v2oEFK4nP2ysH5VnxGJFgXGIfykec +IdUnFtenTde8gCdueqFT2co0inslxVV8ETmEO8dJnPqnnXBunX0n5D3WqDBmJwXK +D2POa7xeXM5tdOxT61S0mSKVgtCQ9VU38OdHy1ECgYAZedpRncCVIUokGTZDu/V8 +kFXwiZJNK0vIhSlGsMDuWm7W4PO5PJ3UaeOm47OcN6XBAhO+PNFDjS62Fa8gIqSl +o8DpU19VtMr180wQlrOEzsBzGP9hUJjBY+apZBwn5+JgaG6xWPaMPsAp19oCkmbv +8NUXP/tAQ0ygtLrsZchDSQKBgHWcJ6j+H1CGaIFHXNOGWmzXRqIp4pOjlGarko2x +VthZ88BCbLCGJLx1W9h95CIBlzFOj9LWlf7PBjOWBqWjl0pxguegeSGtl38uJyfL +kdvitCkoRMU9kxuPkxRDMGe12QIBjZ3IQLzRV1yO0IFO0alvI+D2F17io+REasio +pTaxAoGBANSMXb+sAnXgyu1hK/fOI5QlKNqBfK/PcwrI0nYwHzIqs5Xs5/k5NLH9 +yZyIcDrrk5DJDWJ63gA0WjSL5oHzo5IOHSAkcMc/H7L/4rNFdYNmjkMaTK3Ms4In +xZcz7HaJ1Rxvh/y/lyDn27hsm30WpNfi6lcawBFHCTYdfyWIXj8P +-----END RSA PRIVATE KEY----- diff --git a/examples/digitalidentity/requirements.in b/examples/digitalidentity/requirements.in new file mode 100644 index 00000000..b0d3fbd8 --- /dev/null +++ b/examples/digitalidentity/requirements.in @@ -0,0 +1,3 @@ +flask>=3.0.1 +python-dotenv>=1.0.1 +yoti>=2.15 diff --git a/examples/digitalidentity/requirements.txt b/examples/digitalidentity/requirements.txt new file mode 100644 index 00000000..5cece086 --- /dev/null +++ b/examples/digitalidentity/requirements.txt @@ -0,0 +1,60 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --output-file=requirements.txt requirements.in +# +asn1==2.2.0 + # via yoti +blinker==1.7.0 + # via flask +certifi==2023.11.17 + # via requests +cffi==1.16.0 + # via cryptography +charset-normalizer==3.3.2 + # via requests +click==8.1.7 + # via flask +cryptography==42.0.1 + # via + # pyopenssl + # yoti +deprecated==1.2.10 + # via yoti +flask==3.0.1 + # via -r requirements.in +future==0.18.3 + # via yoti +idna==3.6 + # via requests +iso8601==0.1.13 + # via yoti +itsdangerous==2.1.2 + # via flask +jinja2==3.1.3 + # via flask +markupsafe==2.1.4 + # via + # jinja2 + # werkzeug +protobuf==3.20.1 + # via yoti +pycparser==2.21 + # via cffi +pyopenssl==24.0.0 + # via yoti +python-dotenv==1.0.1 + # via -r requirements.in +pytz==2022.1 + # via yoti +requests==2.31.0 + # via yoti +urllib3==2.1.0 + # via requests +werkzeug==3.0.1 + # via flask +wrapt==1.16.0 + # via deprecated +yoti==2.14.2 + # via -r requirements.in diff --git a/examples/digitalidentity/settings.py b/examples/digitalidentity/settings.py new file mode 100644 index 00000000..cc48730b --- /dev/null +++ b/examples/digitalidentity/settings.py @@ -0,0 +1,16 @@ +from os import environ +from os.path import dirname, join + +from dotenv import load_dotenv + +dotenv_path = join(dirname(__file__), ".env") +load_dotenv(dotenv_path) + +YOTI_CLIENT_SDK_ID = environ.get("YOTI_CLIENT_SDK_ID", None) +YOTI_KEY_FILE_PATH = environ.get("YOTI_KEY_FILE_PATH", None) +YOTI_API_URL = environ.get("YOTI_API_URL", "https://api.yoti.com/share") + +if YOTI_CLIENT_SDK_ID is None or YOTI_KEY_FILE_PATH is None: + raise ValueError("YOTI_CLIENT_SDK_ID or YOTI_KEY_FILE_PATH is None") + +YOTI_APP_BASE_URL = environ.get("YOTI_APP_BASE_URL", "https://127.0.0.1:8000") diff --git a/examples/digitalidentity/static/assets/app-store-badge.png b/examples/digitalidentity/static/assets/app-store-badge.png new file mode 100644 index 00000000..3ec996cc Binary files /dev/null and b/examples/digitalidentity/static/assets/app-store-badge.png differ diff --git a/examples/digitalidentity/static/assets/app-store-badge@2x.png b/examples/digitalidentity/static/assets/app-store-badge@2x.png new file mode 100644 index 00000000..84b34068 Binary files /dev/null and b/examples/digitalidentity/static/assets/app-store-badge@2x.png differ diff --git a/examples/digitalidentity/static/assets/company-logo.jpg b/examples/digitalidentity/static/assets/company-logo.jpg new file mode 100644 index 00000000..551474bf Binary files /dev/null and b/examples/digitalidentity/static/assets/company-logo.jpg differ diff --git a/examples/digitalidentity/static/assets/google-play-badge.png b/examples/digitalidentity/static/assets/google-play-badge.png new file mode 100644 index 00000000..761f237b Binary files /dev/null and b/examples/digitalidentity/static/assets/google-play-badge.png differ diff --git a/examples/digitalidentity/static/assets/google-play-badge@2x.png b/examples/digitalidentity/static/assets/google-play-badge@2x.png new file mode 100644 index 00000000..46707cea Binary files /dev/null and b/examples/digitalidentity/static/assets/google-play-badge@2x.png differ diff --git a/examples/digitalidentity/static/assets/icons/address.svg b/examples/digitalidentity/static/assets/icons/address.svg new file mode 100644 index 00000000..f7d9b2a7 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/address.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/digitalidentity/static/assets/icons/calendar.svg b/examples/digitalidentity/static/assets/icons/calendar.svg new file mode 100644 index 00000000..4f6b9bb7 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/calendar.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/digitalidentity/static/assets/icons/chevron-down-grey.svg b/examples/digitalidentity/static/assets/icons/chevron-down-grey.svg new file mode 100644 index 00000000..6753becb --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/chevron-down-grey.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/examples/digitalidentity/static/assets/icons/document.svg b/examples/digitalidentity/static/assets/icons/document.svg new file mode 100644 index 00000000..4c41271e --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/document.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/digitalidentity/static/assets/icons/email.svg b/examples/digitalidentity/static/assets/icons/email.svg new file mode 100644 index 00000000..c4582d6e --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/email.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/examples/digitalidentity/static/assets/icons/gender.svg b/examples/digitalidentity/static/assets/icons/gender.svg new file mode 100644 index 00000000..af5c5772 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/gender.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/digitalidentity/static/assets/icons/nationality.svg b/examples/digitalidentity/static/assets/icons/nationality.svg new file mode 100644 index 00000000..e57d7522 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/nationality.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/digitalidentity/static/assets/icons/phone.svg b/examples/digitalidentity/static/assets/icons/phone.svg new file mode 100644 index 00000000..b19cce04 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/phone.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/digitalidentity/static/assets/icons/profile.svg b/examples/digitalidentity/static/assets/icons/profile.svg new file mode 100644 index 00000000..5c514fc1 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/examples/digitalidentity/static/assets/icons/verified.svg b/examples/digitalidentity/static/assets/icons/verified.svg new file mode 100644 index 00000000..7ca4dbb3 --- /dev/null +++ b/examples/digitalidentity/static/assets/icons/verified.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/digitalidentity/static/assets/logo.png b/examples/digitalidentity/static/assets/logo.png new file mode 100644 index 00000000..c60227fa Binary files /dev/null and b/examples/digitalidentity/static/assets/logo.png differ diff --git a/examples/digitalidentity/static/assets/logo@2x.png b/examples/digitalidentity/static/assets/logo@2x.png new file mode 100644 index 00000000..9f29784d Binary files /dev/null and b/examples/digitalidentity/static/assets/logo@2x.png differ diff --git a/examples/digitalidentity/static/index.css b/examples/digitalidentity/static/index.css new file mode 100644 index 00000000..e3184163 --- /dev/null +++ b/examples/digitalidentity/static/index.css @@ -0,0 +1,152 @@ +.yoti-body { + margin: 0; +} + +.yoti-top-section { + display: flex; + flex-direction: column; + padding: 38px 0; + background-color: #f7f8f9; + align-items: center; +} + +.yoti-logo-section { + margin-bottom: 25px; +} + +.yoti-logo-image { + display: block; +} + +.yoti-top-header { + font-family: Roboto, sans-serif; + font-size: 40px; + font-weight: 700; + line-height: 1.2; + margin-top: 0; + margin-bottom: 80px; + text-align: center; + color: #000; +} + +@media (min-width: 600px) { + .yoti-top-header { + line-height: 1.4; + } +} + +.yoti-sdk-integration-section { + margin: 30px 0; +} + +#yoti-share-button { + width: 250px; + height: 45px; +} + +.yoti-login-or-separator { + text-transform: uppercase; + font-family: Roboto; + font-size: 16px; + font-weight: bold; + line-height: 1.5; + text-align: center; + margin-top: 30px; +} + +.yoti-login-dialog { + display: grid; + box-sizing: border-box; + width: 100%; + padding: 35px 38px; + border-radius: 5px; + background: #fff; + grid-gap: 25px; +} + +@media (min-width: 600px) { + .yoti-login-dialog { + width: 560px; + padding: 35px 88px; + } +} + +.yoti-login-dialog-header { + font-family: Roboto, sans-serif; + font-size: 24px; + font-weight: 700; + line-height: 1.1; + margin: 0; + color: #000; +} + +.yoti-input { + font-family: Roboto, sans-serif; + font-size: 16px; + line-height: 1.5; + box-sizing: border-box; + padding: 12px 15px; + color: #000; + border: solid 2px #000; + border-radius: 4px; + background-color: #fff; +} + +.yoti-login-actions { + display: flex; + justify-content: space-between; + align-items: center; +} + +.yoti-login-forgot-button { + font-family: Roboto, sans-serif; + font-size: 16px; + text-transform: capitalize; +} + +.yoti-login-button { + font-family: Roboto, sans-serif; + font-size: 16px; + box-sizing: border-box; + width: 145px; + height: 50px; + text-transform: uppercase; + color: #fff; + border: 0; + background-color: #000; +} + +.yoti-sponsor-app-section { + display: flex; + flex-direction: column; + padding: 70px 0; + align-items: center; +} + +.yoti-sponsor-app-header { + font-family: Roboto, sans-serif; + font-size: 20px; + font-weight: 700; + line-height: 1.2; + margin: 0; + text-align: center; + color: #000; +} + +.yoti-store-buttons-section { + margin-top: 40px; + display: grid; + grid-gap: 10px; + grid-template-columns: 1fr; +} + +@media (min-width: 600px) { + .yoti-store-buttons-section { + grid-template-columns: 1fr 1fr; + grid-gap: 25px; + } +} + +.yoti-app-button-link { + text-decoration: none; +} diff --git a/examples/digitalidentity/static/profile.css b/examples/digitalidentity/static/profile.css new file mode 100644 index 00000000..80871acd --- /dev/null +++ b/examples/digitalidentity/static/profile.css @@ -0,0 +1,420 @@ +.yoti-html { + height: 100%; +} + +.yoti-body { + margin: 0; + height: 100%; +} + +.yoti-icon-profile, +.yoti-icon-phone, +.yoti-icon-email, +.yoti-icon-calendar, +.yoti-icon-verified, +.yoti-icon-address, +.yoti-icon-gender, +.yoti-icon-nationality { + display: inline-block; + height: 28px; + width: 28px; + flex-shrink: 0; +} + +.yoti-icon-profile { + background: no-repeat url('/static/assets/icons/profile.svg'); +} + +.yoti-icon-phone { + background: no-repeat url('/static/assets/icons/phone.svg'); +} + +.yoti-icon-email { + background: no-repeat url('/static/assets/icons/email.svg'); +} + +.yoti-icon-calendar { + background: no-repeat url('/static/assets/icons/calendar.svg'); +} + +.yoti-icon-verified { + background: no-repeat url('/static/assets/icons/verified.svg'); +} + +.yoti-icon-address { + background: no-repeat url('/static/assets/icons/address.svg'); +} + +.yoti-icon-gender { + background: no-repeat url('/static/assets/icons/gender.svg'); +} + +.yoti-icon-nationality { + background: no-repeat url('/static/assets/icons/nationality.svg'); +} + +.yoti-profile-layout { + display: grid; + grid-template-columns: 1fr; +} + +@media (min-width: 1100px) { + .yoti-profile-layout { + grid-template-columns: 360px 1fr; + height: 100%; + } +} + +.yoti-profile-user-section { + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: column; + padding: 40px 0; + background-color: #f7f8f9; +} + +@media (min-width: 1100px) { + .yoti-profile-user-section { + display: grid; + grid-template-rows: repeat(3, min-content); + align-items: center; + justify-content: center; + position: relative; + } +} + +.yoti-profile-picture-image { + width: 220px; + height: 220px; + border-radius: 50%; + margin-left: auto; + margin-right: auto; + display: block; +} + +.yoti-profile-picture-powered, +.yoti-profile-picture-account-creation { + font-family: Roboto; + font-size: 14px; + color: #b6bfcb; +} + +.yoti-profile-picture-powered-section { + display: flex; + flex-direction: column; + text-align: center; + align-items: center; +} + +@media (min-width: 1100px) { + .yoti-profile-picture-powered-section { + align-self: start; + } +} + +.yoti-profile-picture-powered { + margin-bottom: 20px; +} + +.yoti-profile-picture-section { + display: flex; + flex-direction: column; + align-items: center; +} + +@media (min-width: 1100px) { + .yoti-profile-picture-section { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 100%; + } +} + +.yoti-logo-image { + margin-bottom: 25px; +} + +.yoti-profile-picture-area { + position: relative; + display: inline-block; +} + +.yoti-profile-picture-verified-icon { + display: block; + background: no-repeat url("/static/assets/icons/verified.svg"); + background-size: cover; + height: 40px; + width: 40px; + position: absolute; + top: 10px; + right: 10px; +} + +.yoti-profile-name { + margin-top: 20px; + font-family: Roboto, sans-serif; + font-size: 24px; + text-align: center; + color: #333b40; +} + +.yoti-attributes-section { + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + padding: 40px 0; +} + + .yoti-attributes-section.-condensed { + padding: 0; + } + +@media (min-width: 1100px) { + .yoti-attributes-section { + padding: 60px 0; + align-items: start; + overflow-y: scroll; + } + + .yoti-attributes-section.-condensed { + padding: 0; + } +} + +.yoti-company-logo { + margin-bottom: 40px; +} + +@media (min-width: 1100px) { + .yoti-company-logo { + margin-left: 130px; + } +} + +/* extended layout list */ +.yoti-attribute-list-header, +.yoti-attribute-list-subheader { + display: none; +} + +@media (min-width: 1100px) { + .yoti-attribute-list-header, + .yoti-attribute-list-subheader { + width: 100%; + display: grid; + grid-template-columns: 200px 1fr 1fr; + grid-template-rows: 40px; + align-items: center; + text-align: center; + font-family: Roboto; + font-size: 14px; + color: #b6bfcb; + } +} + +.yoti-attribute-list-header-attribute, +.yoti-attribute-list-header-value { + justify-self: start; + padding: 0 20px; +} + +.yoti-attribute-list-subheader { + grid-template-rows: 30px; +} + +.yoti-attribute-list-subhead-layout { + grid-column: 3; + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} + +.yoti-attribute-list { + display: grid; + width: 100%; +} + +.yoti-attribute-list-item:first-child { + border-top: 2px solid #f7f8f9; +} + +.yoti-attribute-list-item { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: minmax(60px, auto); + border-bottom: 2px solid #f7f8f9; + border-right: none; + border-left: none; +} + + .yoti-attribute-list-item.-condensed { + grid-template-columns: 50% 50%; + padding: 5px 35px; + } + +@media (min-width: 1100px) { + .yoti-attribute-list-item { + display: grid; + grid-template-columns: 200px 1fr 1fr; + grid-template-rows: minmax(80px, auto); + } + + .yoti-attribute-list-item.-condensed { + grid-template-columns: 200px 1fr; + padding: 0 75px; + } +} + +.yoti-attribute-cell { + display: flex; + align-items: center; +} + +.yoti-attribute-name { + grid-column: 1 / 2; + display: flex; + align-items: center; + justify-content: center; + border-right: 2px solid #f7f8f9; + padding: 20px; +} + +@media (min-width: 1100px) { + .yoti-attribute-name { + justify-content: start; + } +} + +.yoti-attribute-name.-condensed { + justify-content: start; +} + +.yoti-attribute-name-cell { + display: flex; + align-items: center; +} + +.yoti-attribute-name-cell-text { + font-family: Roboto, sans-serif; + font-size: 16px; + color: #b6bfcb; + margin-left: 12px; +} + +.yoti-attribute-value { + grid-column: 2 / 3; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +@media (min-width: 1100px) { + .yoti-attribute-value { + justify-content: start; + } +} + +.yoti-attribute-value.-condensed { + justify-content: start; +} + +.yoti-attribute-value-text { + font-family: Roboto, sans-serif; + font-size: 18px; + color: #333b40; + word-break: break-word; +} + + .yoti-attribute-value-text table { + font-size: 14px; + border-spacing: 0; + } + + .yoti-attribute-value-text table td:first-child { + font-weight: bold; + } + + .yoti-attribute-value-text table td { + border-bottom: 1px solid #f7f8f9; + padding: 5px; + } + + .yoti-attribute-value-text img { + width: 100%; + } + +.yoti-attribute-anchors-layout { + grid-column: 1 / 3; + grid-row: 2 / 2; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-auto-rows: minmax(40px, auto); + font-family: Roboto, sans-serif; + font-size: 14px; + background-color: #f7f8f9; + border: 5px solid white; +} + +@media (min-width: 1100px) { + .yoti-attribute-anchors-layout { + grid-column: 3 / 4; + grid-row: 1 / 2; + } +} + +.yoti-attribute-anchors-head { + border-bottom: 1px solid #dde2e5; + display: flex; + align-items: center; + justify-content: center; +} + +@media (min-width: 1100px) { + .yoti-attribute-anchors-head { + display: none; + } +} + +.yoti-attribute-anchors { + display: flex; + align-items: center; + justify-content: center; +} + +.yoti-attribute-anchors-head.-s-v { + grid-column-start: span 1 s-v; +} + +.yoti-attribute-anchors-head.-value { + grid-column-start: span 1 value; +} + +.yoti-attribute-anchors-head.-subtype { + grid-column-start: span 1 subtype; +} + +.yoti-attribute-anchors.-s-v { + grid-column-start: span 1 s-v; +} + +.yoti-attribute-anchors.-value { + grid-column-start: span 1 value; +} + +.yoti-attribute-anchors.-subtype { + grid-column-start: span 1 subtype; +} + +.yoti-edit-section { + padding: 50px 20px; +} + +@media (min-width: 1100px) { + .yoti-edit-section { + padding: 75px 110px; + } +} diff --git a/examples/digitalidentity/templates/index.html b/examples/digitalidentity/templates/index.html new file mode 100644 index 00000000..4485799f --- /dev/null +++ b/examples/digitalidentity/templates/index.html @@ -0,0 +1,98 @@ + + + + + + + Share v2 Example + + + + + +
+
+
+ Yoti +
+ +

Digital Identity Share Example

+ +
+
+
+ +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
+ + + + + diff --git a/examples/digitalidentity/templates/profile.html b/examples/digitalidentity/templates/profile.html new file mode 100644 index 00000000..b9b93e6c --- /dev/null +++ b/examples/digitalidentity/templates/profile.html @@ -0,0 +1,173 @@ + + + + + + + + Share v2 Example - Profile + + + + + +
+
+
+ Powered by + Yoti +
+ +
+ {% if selfie %} +
+ Yoti + +
+ {% endif %} + +
+ {{ full_name }} +
+
+
+ + +
+ + +
+
Attribute
+
Value
+
Anchors
+
+ +
+
+
S / V
+
Value
+
Sub type
+
+
+ +
+ {% for attribute in attribute_list %} +
+
+
+ + {{ attribute_list[attribute].name }} +
+
+ +
+
+ {% if attribute.name == "Structured Postal Address" %} + + {% elif attribute_list["name"] == "identity_profile_report" %} + + {% for key, value in attribute.value.items() %} + + + + {% endfor %} +
bb{{ key }}
+
{{ value | tojson(indent=2) }}
+
+ {% elif attribute_list[attribute].name == "document_details" %} + + + + + + +
Type{{ attribute_list[attribute].value.document_type }}
Issuing Country{{ attribute_list[attribute].value.issuing_country }}
Issuing Authority{{ attribute_list[attribute].value.issuing_authority }}
Document Number{{ attribute_list[attribute].value.document_number }}
Expiration Date{{ attribute_list[attribute].value.expiration_date }}
+ {% elif attribute_list[attribute].name == "document_images" %} + {% for image in attribute_list[attribute].value %} + + {% endfor %} + {% elif attribute_list[attribute].name == "selfie" %} + + + {% else %} + {{ attribute_list[attribute].value }} + {% endif %} +
+
+ +
+
S / V
+
Value
+
Sub type
+ + {% for source in attribute_list[attribute].anchors if source.anchor_type == "SOURCE" %} +
Source
+
{{ source.value }}
+
{{ source.sub_type }}
+ {% endfor %} + {% for verifier in attribute_list[attribute].anchors if verifier.anchor_type == "VERIFIER" %} +
Verifier
+
{{ verifier.value }}
+
{{ verifier.sub_type }}
+ {% endfor %} +
+
+ {% endfor %} +
+
+ +
+ + + + diff --git a/examples/yoti_example_flask/templates/dynamic-share.html b/examples/yoti_example_flask/templates/dynamic-share.html index 76c7921e..494f7e5d 100644 --- a/examples/yoti_example_flask/templates/dynamic-share.html +++ b/examples/yoti_example_flask/templates/dynamic-share.html @@ -62,7 +62,7 @@

The Yoti app is free to download and use: - + + + + + + diff --git a/examples/yoti_example_flask_di/templates/index.html b/examples/yoti_example_flask_di/templates/index.html new file mode 100644 index 00000000..dd02e3bc --- /dev/null +++ b/examples/yoti_example_flask_di/templates/index.html @@ -0,0 +1,88 @@ + + + + + + + Yoti client example + + + + + + +
+
+
+ Yoti +
+ +

We now accept Yoti

+ +
+
+
+ + + + +
+ +
+

The Yoti app is free to download and use:

+ +
+ + Download on the App Store + + + + get it on Google Play + +
+
+
+ + + + + + + diff --git a/examples/yoti_example_flask_di/templates/profile.html b/examples/yoti_example_flask_di/templates/profile.html new file mode 100644 index 00000000..5819341c --- /dev/null +++ b/examples/yoti_example_flask_di/templates/profile.html @@ -0,0 +1,160 @@ +{% macro parse_document_images(prop) %} + {% for image in prop.value %} + + {% endfor %} +{% endmacro %} + +{% macro parse_structured_address(prop) %} + + {% for key in prop.value %} + + + + + {% endfor %} +
{{ key }}{{ prop.value[key] }}
+{% endmacro %} + +{% macro parse_document_details(prop) %} + + + + + {% if prop.value.expiration_date %} + + {% endif %} + {% if prop.value.issuing_authority %} + + {% endif %} +
Type{{ prop.value.document_type }}
Issuing Country{{ prop.value.issuing_country }}
Document Number{{ prop.value.document_number }}
Expiration Date{{ prop.value.expiration_date }}
Issuing Authority{{ prop.value.issuing_authority }}
+{% endmacro %} + +{% macro parse_age_verification(prop) %} + + + + + + + + + + + + + +
Check Type{{ prop.value.check_type }}
Age{{ prop.value.age }}
Result{{ prop.value.result }}
+{% endmacro %} + +{% macro attribute(name, icon, prop, prevalue="") %} + {% if prop %} + {% if prop.value %} +
+
+
+ + {{ name }} +
+
+ +
+
+ {% if prop.name == "document_images" %} + {{ parse_document_images(prop) }} + {% elif prop.name == "structured_postal_address" %} + {{ parse_structured_address(prop) }} + {% elif prop.name == "document_details" %} + {{ parse_document_details(prop) }} + {% elif prop.name == "age_verified" %} + {{ parse_age_verification(prop) }} + {% else %} + {{ prevalue }} + {{ prop.value }} + {% endif %} +
+
+
+
S / V
+
Value
+
Sub type
+ {% for source in prop.sources %} +
Source
+
{{ source.value }}
+
{{ source.subtype }}
+ {% endfor %} + {% for verifier in prop.verifiers %} +
Verifier
+
{{ verifier.value }}
+
{{ verifier.subtype }}
+ {% endfor %} +
+
+ {% endif %} + {% endif %} +{% endmacro %} + + + + + + + + Yoti client example + + + + + +
+
+
+ Powered by + Yoti +
+ {% if selfie is not none %} +
+ Yoti + +
+ {% endif %} + +
+ {{ full_name.value }} +
+
+ +
+ +
+
Attribute
+
Value
+
Anchors
+
+ +
+
+
S / V
+
Value
+
Sub type
+
+
+ +
+ {% if given_names %}{{ attribute("Given names", "yoti-icon-profile", given_names) }}{% endif %} + {% if family_name %}{{ attribute("Family names", "yoti-icon-profile", family_name) }}{% endif %} + {% if phone_number %}{{ attribute("Mobile number", "yoti-icon-phone", phone_number) }}{% endif %} + {% if email_address %}{{ attribute("Email address", "yoti-icon-email", email_address) }}{% endif %} + {% if date_of_birth %}{{ attribute("Date of birth", "yoti-icon-calendar", date_of_birth) }}{% endif %} + {% if age_verified %}{{ attribute("Age verified", "yoti-icon-verified", age_verified, "Age Verification/") }}{% endif %} + {% if postal_address %}{{ attribute("Address", "yoti-icon-address", postal_address) }}{% endif %} + {% if structured_postal_address %}{{ attribute("Structured Address", "yoti-icon-address", structured_postal_address) }}{% endif %} + {% if gender %}{{ attribute("Gender", "yoti-icon-gender", gender) }}{% endif %} + {% if document_images %}{{ attribute("Document Images", "yoti-icon-profile", document_images) }}{% endif %} + {% if document_details %}{{ attribute("Document Details", "yoti-icon-profile", document_details) }} {% endif %} +
+ +
+
+ + + diff --git a/setup.py b/setup.py index 5a3982cd..78448d7c 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ install_requires=[ "deprecated==1.2.13", "cryptography>=2.2.1", - "protobuf==3.13.0", + "protobuf==3.20.1", "requests>=2.11.1", "future>=0.18.2", "asn1==2.2.0", @@ -44,7 +44,7 @@ "pylint==1.9.4", "pylint-exit>=1.1.0", "python-coveralls==2.9.3", - "coverage==4.5.4", + "coverage==6.0", "mock==2.0.0", "virtualenv==20.15.1", "flake8==4.0.1", diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index e2084235..800e19bb 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -22,6 +22,7 @@ YOTI_PROFILE_ENDPOINT = "/api/v1" YOTI_DOC_SCAN_ENDPOINT = "/idverify/v1" +DIGITAL_IDENTITY_ENDPOINT = "/share" YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"]) YOTI_API_VERSION = environ.get("YOTI_API_VERSION", DEFAULTS["YOTI_API_VERSION"]) @@ -36,6 +37,10 @@ "{0}:{1}{2}".format(YOTI_API_URL, YOTI_API_PORT, YOTI_DOC_SCAN_ENDPOINT), ) +YOTI_DIGITAL_IDENTITY_API_URL = environ.get( + "YOTI_API_URL2", + "{0}:{1}{2}".format(YOTI_API_URL, YOTI_API_PORT, DIGITAL_IDENTITY_ENDPOINT), +) YOTI_API_VERIFY_SSL = environ.get( "YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"] ) diff --git a/yoti_python_sdk/activity_details.py b/yoti_python_sdk/activity_details.py index 5364860e..49eb4f61 100644 --- a/yoti_python_sdk/activity_details.py +++ b/yoti_python_sdk/activity_details.py @@ -79,6 +79,7 @@ def __init__( self.parent_remember_me_id = receipt.get("parent_remember_me_id") self.outcome = receipt.get("sharing_outcome") self.receipt_id = receipt.get("receipt_id") + self.receipt_identifier = receipt.get("receipt_id") self.extra_data = receipt.get("extra_data") timestamp = receipt.get("timestamp") @@ -97,9 +98,14 @@ def remember_me_id(self): def user_id(self): return self.__remember_me_id + @property + @deprecated + def user_id(self): + return self.__remember_me_id + @user_id.setter @deprecated - def user_id(self, value): + def user_identifier(self, value): self.__remember_me_id = value @deprecated @@ -151,6 +157,7 @@ def __iter__(self): yield "parent_remember_me_id", self.parent_remember_me_id yield "outcome", self.outcome yield "receipt_id", self.receipt_id + yield "receipt_identifier", self.receipt_identifier yield "user_profile", self.user_profile yield "profile", self.profile yield "base64_selfie_uri", self.base64_selfie_uri diff --git a/yoti_python_sdk/anchor.py b/yoti_python_sdk/anchor.py index 924330ae..b053de4b 100644 --- a/yoti_python_sdk/anchor.py +++ b/yoti_python_sdk/anchor.py @@ -39,7 +39,9 @@ def __init__( self.__value = value self.__signed_timestamp = signed_timestamp self.__origin_server_certs = origin_server_certs - + self.idx = 0 + self.data = [] + def __iter__(self): return self diff --git a/yoti_python_sdk/attribute.py b/yoti_python_sdk/attribute.py index 5a5a5bf7..2a02b40d 100644 --- a/yoti_python_sdk/attribute.py +++ b/yoti_python_sdk/attribute.py @@ -2,7 +2,7 @@ class Attribute: - def __init__(self, name=None, value=None, anchors=None): + def __init__(self, name=None, value=None, anchors=None, icon = None): if name is None: name = "" if value is None: @@ -12,6 +12,7 @@ def __init__(self, name=None, value=None, anchors=None): self.__name = name self.__value = value self.__anchors = anchors + self.__icon = icon @property def name(self): @@ -36,3 +37,6 @@ def verifiers(self): return list( filter(lambda a: a.anchor_type == config.ANCHOR_VERIFIER, self.__anchors) ) + @property + def icon(self): + return self.__icon diff --git a/yoti_python_sdk/digital_identity/__init__.py b/yoti_python_sdk/digital_identity/__init__.py new file mode 100644 index 00000000..64e5f2c0 --- /dev/null +++ b/yoti_python_sdk/digital_identity/__init__.py @@ -0,0 +1,5 @@ +from .client import DigitalIdentityClient + +__all__ = [ + "DigitalIdentityClient" +] diff --git a/yoti_python_sdk/digital_identity/client.py b/yoti_python_sdk/digital_identity/client.py new file mode 100644 index 00000000..2368f6e2 --- /dev/null +++ b/yoti_python_sdk/digital_identity/client.py @@ -0,0 +1,259 @@ +import yoti_python_sdk + +import json, base64 + +from yoti_python_sdk.http import SignedRequest + +from .create_share_session_result import CreateShareSessionResult +from .get_share_session_result import GetShareSessionResult +from .create_share_qr_code_result import CreateShareQrCodeResult +from .get_share_qr_code_result import GetShareQrCodeResult +from .get_share_receipt_result import GetShareReceiptResult + +from .receipts.receipt_response import ReceiptResponse +from .receipts.receipt_item_key_response import ReceiptItemKeyResponse + +from .receipts.crypto.decryption import build_user_content_from_encrypted_content, unwrap_receipt_key + +class DigitalIdentityClient(object): + """ + Client used for communication with the Yoti Doc Scan service where any + signed request is required + """ + def __init__(self, sdk_id, key, api_url=None): + self.__sdk_id = sdk_id + self.__key = key + if api_url is not None: + self.__api_url = api_url + else: + self.__api_url = yoti_python_sdk.YOTI_DOC_SCAN_API_URL + def create_share_session(self, share_session_config): + """ + Creates a share session + + :param share_session_config: the share session config + :type share_session_config: dict + :return: the create share session result + :rtype: CreateShareSessionResult + """ + + + payload = json.dumps(share_session_config).encode("utf-8") + + request = ( + SignedRequest.builder() + .with_base_url(self.__api_url) + .with_header("Content-Type", "application/json") + .with_header("X-Yoti-Auth-Id", self.__sdk_id) + .with_pem_file(self.__key) + .with_endpoint("/v2/sessions") + .with_param("appId", self.__sdk_id) + .with_post() + .with_payload(payload) + .build() + ) + + response = request.execute() + + #if response.status_code != 201: + # raise Exception("Failed to create session", response) + + if response.status_code != 201: + print(f"Response Status Code: {response.status_code}") + print(f"Response Content: {response.text}") + raise Exception("Failed to create session", response) + + data = json.loads(response.text) + + return CreateShareSessionResult(data) + + def get_share_session(self, session_id): + """ + Retrieves a share session + + :param session_id: the session id + :type session_id: str + :return: the get share session result + :rtype: GetShareSessionResult + """ + + request = ( + SignedRequest.builder() + .with_base_url(self.__api_url) + .with_header("X-Yoti-Auth-Id", self.__sdk_id) + .with_pem_file(self.__key) + .with_endpoint("/v2/sessions/{}".format(session_id)) + .with_param("appId", self.__sdk_id) + .with_get() + .build() + ) + + response = request.execute() + + if response.status_code != 200: + raise Exception("Failed to get session", response) + + data = json.loads(response.text) + + return GetShareSessionResult(data) + + def create_share_qr_code(self,session_id): + """ + Creates a share QR code + + :param session_id: the session id + :type session_id: str + :return: the share QR code result + :rtype: CreateShareQrCodeResult + """ + + #Create an empty payload + payload = json.dumps({}).encode("utf-8") + + request = ( + SignedRequest.builder() + .with_base_url(self.__api_url) + .with_header("Content-Type", "application/json") + .with_header("X-Yoti-Auth-Id", self.__sdk_id) + .with_pem_file(self.__key) + .with_endpoint("/v2/sessions/{}/qr-codes".format(session_id)) + .with_param("appId", self.__sdk_id) + .with_post() + .with_payload(payload) + .build() + ) + + response = request.execute() + + if response.status_code != 201: + raise Exception("Failed to create qr code", response) + + data = json.loads(response.text) + + return CreateShareQrCodeResult(data) + + def get_share_qr_code(self,qrCodeId): + """ + Retrieves a share QR code + + :param qrCodeId: the qr code id + :type qrCodeId: str + :return: the get share QR code result + :rtype: GetShareQrCodeResult + """ + + request = ( + SignedRequest.builder() + .with_base_url(self.__api_url) + .with_header("X-Yoti-Auth-Id", self.__sdk_id) + .with_pem_file(self.__key) + .with_endpoint("/v2/qr-codes/{}".format(qrCodeId)) + .with_param("appId", self.__sdk_id) + .with_get() + .build() + ) + + response = request.execute() + + if response.status_code != 200: + raise Exception("Failed to get session", response) + + data = json.loads(response.text) + + return GetShareQrCodeResult(data) + + def fetch_receipt(self, receiptId): + """ + Fetches the receipt + + :param receiptId: the receipt id + :type receiptId: str + :return: the receipt response + :rtype: ReceiptResponse + """ + + # Convert the receipt id to a url safe base64 string + receiptIdUrl = base64.urlsafe_b64encode(base64.b64decode(receiptId)).decode() + + + request = ( + SignedRequest.builder() + .with_base_url(self.__api_url) + .with_header("X-Yoti-Auth-Id", self.__sdk_id) + .with_pem_file(self.__key) + .with_endpoint("/v2/receipts/{}".format(receiptIdUrl)) + .with_param("appId", self.__sdk_id) + .with_get() + .build() + ) + + response = request.execute() + + if response.status_code != 200: + raise Exception("Failed to get session", response) + + data = json.loads(response.text) + + return ReceiptResponse(data) + + def fetch_receipt_item_key(self, receiptItemKeyId): + """ + Fetches the receipt item key + + :param receiptItemKeyId: the receipt item key id + :type receiptItemKeyId: str + :return: the receipt item key response + :rtype: ReceiptItemKeyResponse + """ + + request = ( + SignedRequest.builder() + .with_base_url(self.__api_url) + .with_header("X-Yoti-Auth-Id", self.__sdk_id) + .with_pem_file(self.__key) + .with_endpoint("/v2/wrapped-item-keys/{}".format(receiptItemKeyId)) + .with_param("appId", self.__sdk_id) + .with_get() + .build() + ) + + response = request.execute() + + if response.status_code != 200: + raise Exception("Failed to get session", response) + + data = json.loads(response.text) + + return ReceiptItemKeyResponse(data) + + def get_share_receipt(self, receiptId): + """ + Retrieves a share receipt + + :param receiptId: the receipt id + :type receiptId: str + :return: the get share receipt result + :rtype: GetShareReceiptResult + """ + + receipt_response = self.fetch_receipt(receiptId) + item_key_id = receipt_response.wrappedItemKeyId + + if (item_key_id is None): + return GetShareReceiptResult(receipt_response) + + encrypted_item_key_response = self.fetch_receipt_item_key(item_key_id) + + receipt_content_key = unwrap_receipt_key( + receipt_response.wrappedKey, + encrypted_item_key_response.value, + encrypted_item_key_response.iv, + self.__key + ) + + user_content = build_user_content_from_encrypted_content( + receipt_response.otherPartyContent, + receipt_content_key, + ) + + return GetShareReceiptResult(receipt_response, user_content) diff --git a/yoti_python_sdk/digital_identity/create_share_qr_code_result.py b/yoti_python_sdk/digital_identity/create_share_qr_code_result.py new file mode 100644 index 00000000..8c3a6e3d --- /dev/null +++ b/yoti_python_sdk/digital_identity/create_share_qr_code_result.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +class CreateShareQrCodeResult(object): + + def __init__(self, data=None): + """ + :param data: the data + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__uri = data.get("uri", None) + + def to_dict(self): + return { + 'id': self.__id, + 'uri': self.__uri, + } + + @property + def id(self): + """ + :return: the qr code id + :rtype: str or None + """ + return self.__id + + @property + def uri(self): + """ + :return: the qr code uri + :rtype: str or None + """ + return self.__uri + \ No newline at end of file diff --git a/yoti_python_sdk/digital_identity/create_share_session_result.py b/yoti_python_sdk/digital_identity/create_share_session_result.py new file mode 100644 index 00000000..be947a1c --- /dev/null +++ b/yoti_python_sdk/digital_identity/create_share_session_result.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +class CreateShareSessionResult(object): + + def __init__(self, data=None): + """ + :param data: the data + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__status = data.get("status", None) + self.__expiry = data.get("expiry", None) + + def to_dict(self): + return { + 'id': self.__id, + 'status': self.__status, + 'expiry': self.__expiry, + } + + @property + def id(self): + """ + :return: the session id + :rtype: str or None + """ + return self.__id + + @property + def status(self): + """ + :return: the session status + :rtype: str or None + """ + return self.__status + + @property + def expiry(self): + """ + :return: the session expiry + :rtype: str or None + """ + return self.__expiry + \ No newline at end of file diff --git a/yoti_python_sdk/digital_identity/get_share_qr_code_result.py b/yoti_python_sdk/digital_identity/get_share_qr_code_result.py new file mode 100644 index 00000000..74a2652a --- /dev/null +++ b/yoti_python_sdk/digital_identity/get_share_qr_code_result.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +class GetShareQrCodeResult(object): + + def __init__(self, data=None): + """ + Initializes the GetShareQrCodeResult object. + + :param data: Dictionary containing QR code data. + :type data: dict or None + """ + data = data or {} + + self.__id = data.get("id") + self.__expiry = data.get("expiry") + self.__sessionId = data.get("sessionId") + self.__redirectUri = data.get("redirectUri") + + def to_dict(self): + """Converts the object to a dictionary representation.""" + return { + 'id': self.__id, + 'expiry': self.__expiry, + 'sessionId': self.__sessionId, + 'redirectUri': self.__redirectUri, + } + + @property + def id(self): + """Returns the QR code ID.""" + return self.__id + + @property + def expiry(self): + """Returns the QR code expiry.""" + return self.__expiry + + @property + def sessionId(self): + """Returns the session ID.""" + return self.__sessionId + + @property + def redirectUri(self): + """Returns the redirect URI.""" + return self.__redirectUri diff --git a/yoti_python_sdk/digital_identity/get_share_receipt_result.py b/yoti_python_sdk/digital_identity/get_share_receipt_result.py new file mode 100644 index 00000000..59b08cdf --- /dev/null +++ b/yoti_python_sdk/digital_identity/get_share_receipt_result.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .receipts.receipt_response import ReceiptResponse +from .receipts.user_content import UserContent + +class GetShareReceiptResult(ReceiptResponse): + + def __init__(self, receipt_response: ReceiptResponse, user_content: UserContent = None): + """ + :param receipt_response: the receipt response + :type receipt_response: ReceiptResponse + :param user_content: the user content, defaults to a new UserContent if None + :type user_content: UserContent or None + """ + self.__userContent = user_content if user_content is not None else UserContent() + + self.__id = receipt_response.id + self.__sessionId = receipt_response.sessionId + self.__timestamp = receipt_response.timestamp + self.__rememberMeId = receipt_response.rememberMeId + self.__parentRememberMeId = receipt_response.parentRememberMeId + + def to_dict(self): + return { + 'id': self.__id, + 'sessionId': self.__sessionId, + 'timestamp': self.__timestamp, + 'rememberMeId': self.__rememberMeId, + 'parentRememberMeId': self.__parentRememberMeId, + } + + @property + def receiptId(self): + """Returns the receipt ID.""" + return self.__id + + @property + def sessionId(self): + """Returns the session ID.""" + return self.__sessionId + + @property + def timestamp(self): + """Returns the timestamp.""" + return self.__timestamp + + @property + def rememberMeId(self): + """Returns the remember me ID.""" + return self.__rememberMeId + + @property + def parentRememberMeId(self): + """Returns the parent remember me ID.""" + return self.__parentRememberMeId + + @property + def userContent(self): + """Returns the user content.""" + return self.__userContent + + @property + def profile(self): + """Returns the user's profile.""" + return self.__userContent.profile if self.__userContent else None + + @property + def extra_data(self): + """Returns the extra data.""" + return self.__userContent.extra_data if self.__userContent else {} diff --git a/yoti_python_sdk/digital_identity/get_share_session_result.py b/yoti_python_sdk/digital_identity/get_share_session_result.py new file mode 100644 index 00000000..9f171ecf --- /dev/null +++ b/yoti_python_sdk/digital_identity/get_share_session_result.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +class GetShareSessionResult(object): + + def __init__(self, data=None): + """ + :param data: the data + :type data: dict or None + """ + self.__data = data if data is not None else {} + + self.__id = self.__data.get("id") + self.__status = self.__data.get("status") + self.__created = self.__data.get("created") + self.__updated = self.__data.get("updated") + self.__expiry = self.__data.get("expiry") + self.__qrCode = self.__data.get("qrCode") + self.__receipt = self.__data.get("receipt") + + def to_dict(self): + return { + 'id': self.__id, + 'status': self.__status, + 'created': self.__created, + 'updated': self.__updated, + 'expiry': self.__expiry, + 'qrCode': self.__qrCode, + 'receipt': self.__receipt, + } + + @property + def id(self): + """Returns the session ID.""" + return self.__id + + @property + def status(self): + """Returns the session status.""" + return self.__status + + @property + def created(self): + """Returns the session creation timestamp.""" + return self.__created + + @property + def updated(self): + """Returns the session last updated timestamp.""" + return self.__updated + + @property + def expiry(self): + """Returns the session expiry timestamp.""" + return self.__expiry + + @property + def qrCode(self): + """Returns the QR code data.""" + return self.__qrCode + + @property + def qrCodeId(self): + """Returns the QR code ID, if available.""" + return self.__qrCode.get("id") if self.__qrCode else None + + @property + def receipt(self): + """Returns the receipt data.""" + return self.__receipt + + @property + def receiptId(self): + """Returns the receipt ID, if available.""" + return self.__receipt.get("id") if self.__receipt else None diff --git a/yoti_python_sdk/digital_identity/receipts/__init__.py b/yoti_python_sdk/digital_identity/receipts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/digital_identity/receipts/base_content.py b/yoti_python_sdk/digital_identity/receipts/base_content.py new file mode 100644 index 00000000..d72063f7 --- /dev/null +++ b/yoti_python_sdk/digital_identity/receipts/base_content.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +class BaseContent(): + + def __init__(self, extra_data=None): + """ + :param data: the extra data + :type data: dict or None + """ + + if extra_data is None: + extra_data = dict() + + self.__extra_data = extra_data + + def to_dict(self): + return { + 'extra_data': self.__extra_data, + } + + @property + def extra_data(self): + """ + :return: the extra data + :rtype: dict + """ + return self.__extra_data + \ No newline at end of file diff --git a/yoti_python_sdk/digital_identity/receipts/crypto/__init__.py b/yoti_python_sdk/digital_identity/receipts/crypto/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/digital_identity/receipts/crypto/decryption.py b/yoti_python_sdk/digital_identity/receipts/crypto/decryption.py new file mode 100644 index 00000000..7fa974f3 --- /dev/null +++ b/yoti_python_sdk/digital_identity/receipts/crypto/decryption.py @@ -0,0 +1,86 @@ +import base64 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from yoti_python_sdk.protobuf import protobuf +from yoti_python_sdk.profile import Profile +from ..user_content import UserContent +from .utils import decrypt_receipt_content + + +def unwrap_receipt_key(wrapped_receipt_key, encrypted_item_key, item_key_iv, pem): + # Decode the base64 encoded inputs + wrapped_receipt_key_buffer = base64.b64decode(wrapped_receipt_key) + encrypted_item_key_buffer = base64.b64decode(encrypted_item_key) + item_key_iv_buffer = base64.b64decode(item_key_iv) + + # Load the private key from PEM file + private_key = load_private_key(pem) + + # Decrypt the item key + decrypted_item_key = decrypt_item_key(private_key, encrypted_item_key_buffer) + + # Decrypt the wrapped receipt key + return decrypt_wrapped_receipt_key(decrypted_item_key, wrapped_receipt_key_buffer, item_key_iv_buffer) + + +def load_private_key(pem): + with open(pem, "rb") as key_file: + return serialization.load_pem_private_key( + key_file.read(), + password=None, + backend=default_backend() + ) + + +def decrypt_item_key(private_key, encrypted_item_key_buffer): + return private_key.decrypt( + encrypted_item_key_buffer, + padding.PKCS1v15() + ) + + +def decrypt_wrapped_receipt_key(decrypted_item_key, wrapped_receipt_key_buffer, item_key_iv_buffer): + tag_size = 16 # Size of the authentication tag + + # Separate the authentication tag from the ciphertext + cipher_text, tag = wrapped_receipt_key_buffer[:-tag_size], wrapped_receipt_key_buffer[-tag_size:] + + # Create the cipher and decrypt the data + cipher = Cipher(algorithms.AES(decrypted_item_key), modes.GCM(item_key_iv_buffer, tag), backend=default_backend()) + decryptor = cipher.decryptor() + + return decryptor.update(cipher_text) + decryptor.finalize() + + +def build_user_content_from_encrypted_content(content, receipt_content_key): + if content is None: + content = {'profile': '', 'extraData': ''} + + attributes, extra_data = decrypt_and_extract_content_data(content, receipt_content_key) + return UserContent(attributes, extra_data) + + +def decrypt_and_extract_content_data(content=None, receipt_content_key=None): + if content is None: + content = {'profile': '', 'extraData': ''} + + decrypted_profile = decrypt_receipt_content(content.get('profile'), receipt_content_key) + decrypted_extra_data = decrypt_receipt_content(content.get('extraData'), receipt_content_key) + + attributes = extract_attributes(decrypted_profile) + extracted_extra_data = decrypted_extra_data if decrypted_extra_data else None + + return attributes, extracted_extra_data + + +def extract_attributes(decrypted_profile): + proto = protobuf.Protobuf() + + if decrypted_profile: + user_profile_attribute_list = proto.attribute_list(decrypted_profile) + return user_profile_attribute_list.attributes if hasattr(user_profile_attribute_list, 'attributes') else None + + return None diff --git a/yoti_python_sdk/digital_identity/receipts/crypto/utils.py b/yoti_python_sdk/digital_identity/receipts/crypto/utils.py new file mode 100644 index 00000000..7609cf19 --- /dev/null +++ b/yoti_python_sdk/digital_identity/receipts/crypto/utils.py @@ -0,0 +1,42 @@ +import base64 +from google.protobuf.message import DecodeError +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +from yoti_python_sdk.protobuf.common_public_api import EncryptedData_pb2 +def decrypt_receipt_content(content, receipt_content_key): + if not content: + return None + + content_buffer = base64.b64decode(content) + + iv, cipher_text = decode_encrypted_data(content_buffer) + + cipher_text_buffer = base64.b64decode(cipher_text) + iv_buffer = base64.b64decode(iv) + + return decrypt_aes_cbc(cipher_text_buffer, iv_buffer, receipt_content_key) + +def decode_encrypted_data(binary_data): + encrypted_data = EncryptedData_pb2.EncryptedData() + + try: + encrypted_data.ParseFromString(binary_data) + except DecodeError: + raise ValueError("Failed to decode binary data") + + return ( + base64.b64encode(encrypted_data.iv).decode('utf-8'), + base64.b64encode(encrypted_data.cipher_text).decode('utf-8'), + ) + +def decrypt_aes_cbc(cipher_text, iv, secret): + cipher = Cipher(algorithms.AES(secret), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + decrypted_data = decryptor.update(cipher_text) + decryptor.finalize() + + return strip_pkcs5_padding(decrypted_data) + +def strip_pkcs5_padding(data): + padding_length = data[-1] + return data[:-padding_length] diff --git a/yoti_python_sdk/digital_identity/receipts/receipt_item_key_response.py b/yoti_python_sdk/digital_identity/receipts/receipt_item_key_response.py new file mode 100644 index 00000000..25a4d38c --- /dev/null +++ b/yoti_python_sdk/digital_identity/receipts/receipt_item_key_response.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class ReceiptItemKeyResponse: + def __init__(self, data=None): + """ + Initializes the ReceiptItemKeyResponse with provided data. + + :param data: The data containing id, iv, and value + :type data: dict or None + """ + self.__id = data.get("id") if data else None + self.__iv = data.get("iv") if data else None + self.__value = data.get("value") if data else None + + def to_dict(self): + """Returns the object data as a dictionary.""" + return { + 'id': self.__id, + 'iv': self.__iv, + 'value': self.__value, + } + + @property + def id(self): + """Returns the id.""" + return self.__id + + @property + def iv(self): + """Returns the iv.""" + return self.__iv + + @property + def value(self): + """Returns the value.""" + return self.__value diff --git a/yoti_python_sdk/digital_identity/receipts/receipt_response.py b/yoti_python_sdk/digital_identity/receipts/receipt_response.py new file mode 100644 index 00000000..681254c2 --- /dev/null +++ b/yoti_python_sdk/digital_identity/receipts/receipt_response.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +class ReceiptResponse(object): + + def __init__(self, data=None): + """ + :param data: the data + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__sessionId = data.get("sessionId", None) + self.__timestamp = data.get("timestamp", None) + self.__rememberMeId = data.get("rememberMeId", None) + self.__parentRememberMeId = data.get("parentRememberMeId", None) + self.__content = data.get("content", None) + self.__otherPartyContent = data.get("otherPartyContent", None) + self.__wrappedItemKeyId = data.get("wrappedItemKeyId", None) + self.__wrappedKey = data.get("wrappedKey", None) + + def to_dict(self): + return { + 'id': self.__id, + 'sessionId': self.__sessionId, + 'timestamp': self.__timestamp, + 'rememberMeId': self.__rememberMeId, + 'parentRememberMeId': self.__parentRememberMeId, + 'content': self.__content, + 'otherPartyContent': self.__otherPartyContent, + 'wrappedItemKeyId': self.__wrappedItemKeyId, + 'wrappedKey': self.__wrappedKey, + } + + @property + def id(self): + """ + :return: the id + :rtype: str or None + """ + return self.__id + + @property + def sessionId(self): + """ + :return: the session id + :rtype: str or None + """ + return self.__sessionId + + @property + def timestamp(self): + """ + :return: the timestamp + :rtype: str or None + """ + return self.__timestamp + + @property + def rememberMeId(self): + """ + :return: the remember me id + :rtype: str or None + """ + return self.__rememberMeId + + @property + def parentRememberMeId(self): + """ + :return: the parent remember me id + :rtype: str or None + """ + return self.__parentRememberMeId + + @property + def content(self): + """ + :return: the content + :rtype: str or None + """ + return self.__content + + @property + def otherPartyContent(self): + """ + :return: the other party content + :rtype: str or None + """ + return self.__otherPartyContent + + @property + def wrappedItemKeyId(self): + """ + :return: the wrapped item key id + :rtype: str or None + """ + return self.__wrappedItemKeyId + + @property + def wrappedKey(self): + """ + :return: the wrapped key + :rtype: str or None + """ + return self.__wrappedKey diff --git a/yoti_python_sdk/digital_identity/receipts/user_content.py b/yoti_python_sdk/digital_identity/receipts/user_content.py new file mode 100644 index 00000000..9d19c2bd --- /dev/null +++ b/yoti_python_sdk/digital_identity/receipts/user_content.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.profile import Profile + +from .base_content import BaseContent + +class UserContent(BaseContent): + + def __init__(self, attributes=None, extra_data=None): + """ + :param data: the attributes + :type data: array or None + :param extra_data: the extra data + :type extra_data: dict or None + """ + super().__init__(extra_data) + if attributes is None: + attributes = [] + + self.__profile = Profile(attributes) + + def to_dict(self): + return { + 'profile': self.__profile, + } + + @property + def profile(self): + """ + :return: the profile + :rtype: Profile + """ + return self.__profile + \ No newline at end of file diff --git a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py index 854387da..d88236df 100644 --- a/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py +++ b/yoti_python_sdk/dynamic_sharing_service/dynamic_scenario_builder.py @@ -5,6 +5,7 @@ class DynamicScenarioBuilder(object): + print("Hello, World!") def __init__(self): self.__scenario = { "policy": DynamicPolicyBuilder().build(), diff --git a/yoti_python_sdk/profile.py b/yoti_python_sdk/profile.py index 2f230dc9..83491c67 100644 --- a/yoti_python_sdk/profile.py +++ b/yoti_python_sdk/profile.py @@ -15,6 +15,7 @@ def __init__(self, profile_attributes): if profile_attributes: for field in profile_attributes: + # print("field %s" % field) try: value = attribute_parser.value_based_on_content_type( field.value, field.content_type @@ -33,7 +34,6 @@ def __init__(self, profile_attributes): anchors = Anchor().parse_anchors(field.anchors) self.attributes[field.name] = Attribute(field.name, value, anchors) - except ValueError as ve: if logging.getLogger().propagate: logging.warning(ve) diff --git a/yoti_python_sdk/tests/digital_identity/__init__.py b/yoti_python_sdk/tests/digital_identity/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/digital_identity/test_create_share_qr_code.py b/yoti_python_sdk/tests/digital_identity/test_create_share_qr_code.py new file mode 100644 index 00000000..7d2dfcb0 --- /dev/null +++ b/yoti_python_sdk/tests/digital_identity/test_create_share_qr_code.py @@ -0,0 +1,50 @@ +import pytest +from yoti_python_sdk.digital_identity.create_share_qr_code_result import CreateShareQrCodeResult + +def test_create_share_qr_code_result_with_data(): + # Arrange + data = { + 'id': 'test_qr_code_id', + 'uri': 'https://example.com/qr_code_uri' + } + + # Act + result = CreateShareQrCodeResult(data) + + # Assert + assert result.id == 'test_qr_code_id' + assert result.uri == 'https://example.com/qr_code_uri' + assert result.to_dict() == { + 'id': 'test_qr_code_id', + 'uri': 'https://example.com/qr_code_uri' + } + +def test_create_share_qr_code_result_without_data(): + # Act + result = CreateShareQrCodeResult() + + # Assert + assert result.id is None + assert result.uri is None + assert result.to_dict() == { + 'id': None, + 'uri': None + } + +def test_create_share_qr_code_result_partial_data(): + # Arrange + data = { + 'id': 'test_qr_code_id' + # uri is missing + } + + # Act + result = CreateShareQrCodeResult(data) + + # Assert + assert result.id == 'test_qr_code_id' + assert result.uri is None + assert result.to_dict() == { + 'id': 'test_qr_code_id', + 'uri': None + } diff --git a/yoti_python_sdk/tests/digital_identity/test_create_share_session.py b/yoti_python_sdk/tests/digital_identity/test_create_share_session.py new file mode 100644 index 00000000..5b786f35 --- /dev/null +++ b/yoti_python_sdk/tests/digital_identity/test_create_share_session.py @@ -0,0 +1,58 @@ +import pytest +from yoti_python_sdk.digital_identity.create_share_session_result import CreateShareSessionResult + +def test_create_share_session_result_with_data(): + # Arrange + data = { + 'id': 'test_session_id', + 'status': 'ACTIVE', + 'expiry': '2024-12-31T23:59:59Z' + } + + # Act + result = CreateShareSessionResult(data) + + # Assert + assert result.id == 'test_session_id' + assert result.status == 'ACTIVE' + assert result.expiry == '2024-12-31T23:59:59Z' + assert result.to_dict() == { + 'id': 'test_session_id', + 'status': 'ACTIVE', + 'expiry': '2024-12-31T23:59:59Z' + } + +def test_create_share_session_result_without_data(): + # Act + result = CreateShareSessionResult() + + # Assert + assert result.id is None + assert result.status is None + assert result.expiry is None + assert result.to_dict() == { + 'id': None, + 'status': None, + 'expiry': None + } + +def test_create_share_session_result_partial_data(): + # Arrange + data = { + 'id': 'test_session_id', + 'status': 'ACTIVE' + # expiry is missing + } + + # Act + result = CreateShareSessionResult(data) + + # Assert + assert result.id == 'test_session_id' + assert result.status == 'ACTIVE' + assert result.expiry is None + assert result.to_dict() == { + 'id': 'test_session_id', + 'status': 'ACTIVE', + 'expiry': None + } diff --git a/yoti_python_sdk/tests/digital_identity/test_get_share_qr_code.py b/yoti_python_sdk/tests/digital_identity/test_get_share_qr_code.py new file mode 100644 index 00000000..0e4eb7db --- /dev/null +++ b/yoti_python_sdk/tests/digital_identity/test_get_share_qr_code.py @@ -0,0 +1,65 @@ +import pytest +from yoti_python_sdk.digital_identity.get_share_qr_code_result import GetShareQrCodeResult + +def test_get_share_qr_code_result_with_data(): + # Arrange + data = { + 'id': 'test_qr_code_id', + 'expiry': '2024-12-31T23:59:59Z', + 'sessionId': 'test_session_id', + 'redirectUri': 'https://example.com' + } + + # Act + result = GetShareQrCodeResult(data) + + # Assert + assert result.id == 'test_qr_code_id' + assert result.expiry == '2024-12-31T23:59:59Z' + assert result.sessionId == 'test_session_id' + assert result.redirectUri == 'https://example.com' + assert result.to_dict() == { + 'id': 'test_qr_code_id', + 'expiry': '2024-12-31T23:59:59Z', + 'sessionId': 'test_session_id', + 'redirectUri': 'https://example.com' + } + +def test_get_share_qr_code_result_without_data(): + # Act + result = GetShareQrCodeResult() + + # Assert + assert result.id is None + assert result.expiry is None + assert result.sessionId is None + assert result.redirectUri is None + assert result.to_dict() == { + 'id': None, + 'expiry': None, + 'sessionId': None, + 'redirectUri': None + } + +def test_get_share_qr_code_result_partial_data(): + # Arrange + data = { + 'id': 'test_qr_code_id', + 'sessionId': 'test_session_id' + # expiry and redirectUri are missing + } + + # Act + result = GetShareQrCodeResult(data) + + # Assert + assert result.id == 'test_qr_code_id' + assert result.expiry is None + assert result.sessionId == 'test_session_id' + assert result.redirectUri is None + assert result.to_dict() == { + 'id': 'test_qr_code_id', + 'expiry': None, + 'sessionId': 'test_session_id', + 'redirectUri': None + } diff --git a/yoti_python_sdk/tests/digital_identity/test_get_share_session.py b/yoti_python_sdk/tests/digital_identity/test_get_share_session.py new file mode 100644 index 00000000..0b6c7716 --- /dev/null +++ b/yoti_python_sdk/tests/digital_identity/test_get_share_session.py @@ -0,0 +1,29 @@ +import pytest +from yoti_python_sdk.digital_identity.get_share_session_result import GetShareSessionResult + +def test_get_share_session_result_with_valid_data(): + # Arrange + session_data = { + 'id': 'test_session_id', + 'status': 'active', + 'created': '2024-10-24T10:00:00Z', + 'updated': '2024-10-24T11:00:00Z', + 'expiry': '2024-10-24T12:00:00Z', + 'qrCode': {'id': 'test_qr_code_id'}, + 'receipt': {'id': 'test_receipt_id'} + } + + # Act + session_result = GetShareSessionResult(session_data) + + # Assert + assert session_result.id == 'test_session_id' + assert session_result.status == 'active' + assert session_result.created == '2024-10-24T10:00:00Z' + assert session_result.updated == '2024-10-24T11:00:00Z' + assert session_result.expiry == '2024-10-24T12:00:00Z' + assert session_result.qrCode == {'id': 'test_qr_code_id'} + assert session_result.receipt == {'id': 'test_receipt_id'} + assert session_result.qrCodeId == 'test_qr_code_id' + assert session_result.receiptId == 'test_receipt_id' + assert session_result.to_dict() == session_data diff --git a/yoti_python_sdk/tests/digital_identity/test_share_receipt.py b/yoti_python_sdk/tests/digital_identity/test_share_receipt.py new file mode 100644 index 00000000..808d7cb0 --- /dev/null +++ b/yoti_python_sdk/tests/digital_identity/test_share_receipt.py @@ -0,0 +1,32 @@ +import pytest +from yoti_python_sdk.digital_identity.receipts.receipt_response import ReceiptResponse +from yoti_python_sdk.digital_identity.get_share_receipt_result import GetShareReceiptResult + +def test_get_share_receipt_result_with_receipt_response_and_default_user_content(): + # Arrange + receipt_data = { + 'id': 'test_receipt_id', + 'sessionId': 'test_session_id', + 'timestamp': '2024-10-24T10:00:00Z', + 'rememberMeId': 'test_remember_me_id', + 'parentRememberMeId': 'test_parent_remember_me_id' + } + receipt_response = ReceiptResponse(receipt_data) + + # Act + result = GetShareReceiptResult(receipt_response) + + # Assert + assert result.receiptId == 'test_receipt_id' + assert result.sessionId == 'test_session_id' + assert result.timestamp == '2024-10-24T10:00:00Z' + assert result.rememberMeId == 'test_remember_me_id' + assert result.parentRememberMeId == 'test_parent_remember_me_id' + assert result.userContent is not None + assert result.to_dict() == { + 'id': 'test_receipt_id', + 'sessionId': 'test_session_id', + 'timestamp': '2024-10-24T10:00:00Z', + 'rememberMeId': 'test_remember_me_id', + 'parentRememberMeId': 'test_parent_remember_me_id' + } diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py index 04456ae3..49ae918a 100644 --- a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py @@ -99,8 +99,8 @@ def test_should_filter_checks(self): assert len(result.liveness_checks) == 1 assert isinstance(result.liveness_checks[0], LivenessCheckResponse) - assert len(result.text_data_checks) == 1 - assert isinstance(result.text_data_checks[0], TextDataCheckResponse) + assert len(result.id_document_text_data_checks) == 1 + assert isinstance(result.id_document_text_data_checks[0], TextDataCheckResponse) assert len(result.id_document_text_data_checks) == 1 assert isinstance(result.id_document_text_data_checks[0], TextDataCheckResponse) diff --git a/yoti_python_sdk/tests/test-key.pem b/yoti_python_sdk/tests/test-key.pem new file mode 100644 index 00000000..98c76418 --- /dev/null +++ b/yoti_python_sdk/tests/test-key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/yoti_python_sdk/tests/test_client.py b/yoti_python_sdk/tests/test_client.py index 52ff9115..32fe13e0 100644 --- a/yoti_python_sdk/tests/test_client.py +++ b/yoti_python_sdk/tests/test_client.py @@ -165,11 +165,11 @@ def test_requesting_activity_details_with_correct_data( assert isinstance(activity_details, ActivityDetails) assert ( - activity_details.user_id + activity_details.user_identifier == "Hig2yAT79cWvseSuXcIuCLa5lNkAPy70rxetUaeHlTJGmiwc/g1MWdYWYrexWvPU" ) assert ( - activity_details.receipt_id + activity_details.receipt_identifier == "9HNJDX5bEIN5TqBm0OGzVIc1LaAmbzfx6eIrwNdwpHvKeQmgPujyogC+r7hJCVPl" ) assert activity_details.timestamp == datetime(2016, 7, 19, 8, 55, 38) @@ -213,11 +213,11 @@ def test_requesting_activity_details_with_null_profile( activity_details = client.get_activity_details(encrypted_request_token) assert ( - activity_details.user_id + activity_details.user_identifier == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" ) assert ( - activity_details.receipt_id + activity_details.receipt_identifier == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" ) assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) @@ -271,11 +271,11 @@ def test_requesting_activity_details_with_missing_profile( activity_details = client.get_activity_details(encrypted_request_token) assert ( - activity_details.user_id + activity_details.user_identifier == "ijH4kkqMKTG0FSNUgQIvd2Z3Nx1j8f5RjVQMyoKOvO/hkv43Ik+t6d6mGfP2tdrN" ) assert ( - activity_details.receipt_id + activity_details.receipt_identifier == "Eq3+P8qjAlxr4d2mXKCUvzKdJTchI53ghwYPZXyA/cF5T+m/HCP1bK5LOmudZASN" ) assert activity_details.timestamp == datetime(2016, 11, 14, 11, 35, 33) diff --git a/yoti_python_sdk/tests/test_profile.py b/yoti_python_sdk/tests/test_profile.py index d34d4a4e..18246d3f 100644 --- a/yoti_python_sdk/tests/test_profile.py +++ b/yoti_python_sdk/tests/test_profile.py @@ -678,8 +678,7 @@ def test_get_age_over_verification(attribute_value, expected_age_over, expected_ ) human_profile = Profile(attribute_list) - print(human_profile.attributes) - + age_verifications = human_profile.get_age_verifications() age_verification = human_profile.find_age_over_verification(expected_age_over)