diff --git a/README.md b/README.md index c7130a9..a5b71a4 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,4 @@ Shopify components managed by SPyLib: * Install an app through OAuth * Session tokens * Webhooks +* Multipass diff --git a/docs/index.md b/docs/index.md index c7130a9..a5b71a4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,3 +22,4 @@ Shopify components managed by SPyLib: * Install an app through OAuth * Session tokens * Webhooks +* Multipass diff --git a/docs/multipass.md b/docs/multipass.md new file mode 100644 index 0000000..817aa98 --- /dev/null +++ b/docs/multipass.md @@ -0,0 +1,23 @@ +# Multipass +[Shopify Multipass - Shopify Documentation](https://shopify.dev/docs/admin-api/rest/reference/plus/multipass)
+This helper class generates token or URL that's needed for Shopify Multipass login. + +```python +from spylib import multipass + + +# Customer email is required to generate token or URL +customer_data = {'email': 'customer@email.com'} + +# Generate URL +url = multipass.generate_url( + secret='MULTIPASS_SECRET', + customer_data=customer_data, + store_url='https://example.myshopify.com' +) +# https://example.myshopify.com/account/login/multipass/{MultipassToken} + +# If for some reason you need the token, you can also generate the token used in the URL separately: +token = multipass.generate_token(secret='MULTIPASS_SECRET', customer_data=customer_data) + +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7ec2019..bd6f7f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ starlette = ">= 0.15.0" nest-asyncio = "^1.5.1" pydantic ="^1.8.2" PyJWT = "^2.1.0" +pycryptodome = "^3.10.1" [tool.poetry.dev-dependencies] blue = "0.9.0" diff --git a/spylib/__init__.py b/spylib/__init__.py index bfa8153..414ed0d 100644 --- a/spylib/__init__.py +++ b/spylib/__init__.py @@ -3,6 +3,7 @@ __version__ = '0.6.0' +from .multipass import generate_token, generate_url from .token import ( OfflineTokenABC, OnlineTokenABC, @@ -21,4 +22,6 @@ 'WebhookResponse', 'WebhookTopic', 'is_webhook_valid', + 'generate_token', + 'generate_url', ] diff --git a/spylib/multipass.py b/spylib/multipass.py new file mode 100644 index 0000000..d09c358 --- /dev/null +++ b/spylib/multipass.py @@ -0,0 +1,43 @@ +import datetime +import json +from base64 import urlsafe_b64encode +from typing import Any, Dict + +from Crypto.Cipher import AES +from Crypto.Hash import HMAC, SHA256 +from Crypto.Random import get_random_bytes + + +def generate_token(secret: str, customer_data: Dict[str, Any]) -> bytes: + key = SHA256.new(secret.encode('utf-8')).digest() + encryption_key = key[0:16] + signature_key = key[16:32] + + if 'email' not in customer_data: + raise ValueError('Missing email in customer data') + + customer_data['created_at'] = datetime.datetime.utcnow().isoformat() + cypher_text = _encrypt(encryption_key, json.dumps(customer_data)) + return urlsafe_b64encode(cypher_text + _sign(signature_key, cypher_text)) + + +def generate_url(secret: str, customer_data: Dict[str, Any], store_url) -> str: + token = generate_token(secret, customer_data).decode('utf-8') + return f'{store_url}/account/login/multipass/{token}' + + +def _encrypt(encryption_key, plain_text) -> bytes: + plain_text = _pad(plain_text) + iv = get_random_bytes(AES.block_size) + cipher = AES.new(encryption_key, AES.MODE_CBC, iv) + return iv + cipher.encrypt(str.encode(plain_text)) + + +def _sign(signature_key, cypher_text): + return HMAC.new(signature_key, cypher_text, SHA256).digest() + + +def _pad(s): + return s + (AES.block_size - len(s) % AES.block_size) * chr( + AES.block_size - len(s) % AES.block_size + )