Skip to content
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ Shopify components managed by SPyLib:
* Install an app through OAuth
* Session tokens
* Webhooks
* Multipass
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ Shopify components managed by SPyLib:
* Install an app through OAuth
* Session tokens
* Webhooks
* Multipass
23 changes: 23 additions & 0 deletions docs/multipass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Multipass
[Shopify Multipass - Shopify Documentation](https://shopify.dev/docs/admin-api/rest/reference/plus/multipass) <br>
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)

```
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions spylib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
__version__ = '0.6.0'


from .multipass import generate_token, generate_url
from .token import (
OfflineTokenABC,
OnlineTokenABC,
Expand All @@ -21,4 +22,6 @@
'WebhookResponse',
'WebhookTopic',
'is_webhook_valid',
'generate_token',
'generate_url',
]
43 changes: 43 additions & 0 deletions spylib/multipass.py
Original file line number Diff line number Diff line change
@@ -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
)