Skip to content

Commit 1b693b5

Browse files
committed
support for session tokens from ui extensions added
1 parent 5f29593 commit 1b693b5

File tree

4 files changed

+70
-8
lines changed

4 files changed

+70
-8
lines changed

docs/session-tokens.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Session tokens
22

3-
The Shopify Python API library provides helper methods to decode [session tokens](https://shopify.dev/concepts/apps/building-embedded-apps-using-session-tokens). You can use the `decode_from_header` function to extract and decode a session token from an HTTP Authorization header.
3+
The Shopify Python API library provides helper methods to decode [session tokens](https://shopify.dev/concepts/apps/building-embedded-apps-using-session-tokens). You can use the `decode_from_header` function to extract and decode a session token from an HTTP Authorization header (it can be from a UI Extension or an embedded app).
44

55
## Basic usage
66

@@ -11,6 +11,7 @@ decoded_payload = session_token.decode_from_header(
1111
authorization_header=your_auth_request_header,
1212
api_key=your_api_key,
1313
secret=your_api_secret,
14+
is_extension=True_or_False
1415
)
1516
```
1617

@@ -29,7 +30,8 @@ def session_token_required(func):
2930
decoded_session_token = session_token.decode_from_header(
3031
authorization_header = request.headers.get('Authorization'),
3132
api_key = SHOPIFY_API_KEY,
32-
secret = SHOPIFY_API_SECRET
33+
secret = SHOPIFY_API_SECRET,
34+
is_extension=False
3335
)
3436
with shopify_session(decoded_session_token):
3537
return func(*args, **kwargs)

shopify/session_token.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ALGORITHM = "HS256"
1515
PREFIX = "Bearer "
1616
REQUIRED_FIELDS = ["iss", "dest", "sub", "jti", "sid"]
17+
EXTENSION_REQUIRED_FIELDS = ["aud", "dest", "jti", "exp", "nbf", "iat"]
1718
LEEWAY_SECONDS = 10
1819

1920

@@ -33,10 +34,11 @@ class TokenAuthenticationError(SessionTokenError):
3334
pass
3435

3536

36-
def decode_from_header(authorization_header, api_key, secret):
37+
def decode_from_header(authorization_header, api_key, secret, is_extension=False):
3738
session_token = _extract_session_token(authorization_header)
38-
decoded_payload = _decode_session_token(session_token, api_key, secret)
39-
_validate_issuer(decoded_payload)
39+
decoded_payload = _decode_session_token(session_token, api_key, secret, is_extension)
40+
# skip validation for tokens coming from ui-extensions
41+
_validate_issuer(decoded_payload) if not is_extension else None
4042

4143
return decoded_payload
4244

@@ -48,7 +50,8 @@ def _extract_session_token(authorization_header):
4850
return authorization_header[len(PREFIX) :]
4951

5052

51-
def _decode_session_token(session_token, api_key, secret):
53+
def _decode_session_token(session_token, api_key, secret, is_extension):
54+
required_fields = EXTENSION_REQUIRED_FIELDS if is_extension else REQUIRED_FIELDS
5255
try:
5356
return jwt.decode(
5457
session_token,
@@ -58,7 +61,7 @@ def _decode_session_token(session_token, api_key, secret):
5861
# AppBridge frequently sends future `nbf`, and it causes `ImmatureSignatureError`.
5962
# Accept few seconds clock skew to avoid this error.
6063
leeway=LEEWAY_SECONDS,
61-
options={"require": REQUIRED_FIELDS},
64+
options={"require": required_fields},
6265
)
6366
except jwt.exceptions.PyJWTError as exception:
6467
six.raise_from(SessionTokenError(str(exception)), exception)

test/session_token_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_raises_if_aud_doesnt_match_api_key(self):
7979
with self.assertRaises(session_token.SessionTokenError) as cm:
8080
session_token.decode_from_header(self.build_auth_header(), api_key=self.api_key, secret=self.secret)
8181

82-
self.assertEqual("Invalid audience", str(cm.exception))
82+
self.assertEqual("Audience doesn't match", str(cm.exception))
8383

8484
def test_raises_if_issuer_hostname_is_invalid(self):
8585
self.payload["iss"] = "bad_shop_hostname"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from shopify import session_token
2+
from test.test_helper import TestCase
3+
from datetime import datetime, timedelta
4+
5+
import jwt
6+
import sys
7+
8+
if sys.version_info[0] < 3: # Backwards compatibility for python < v3.0.0
9+
import time
10+
11+
12+
def timestamp(date):
13+
return time.mktime(date.timetuple()) if sys.version_info[0] < 3 else date.timestamp()
14+
15+
16+
class UIExtensionAccessTokenTest(TestCase):
17+
@classmethod
18+
def setUpClass(cls):
19+
cls.secret = "API Secret"
20+
cls.api_key = "API key"
21+
22+
@classmethod
23+
def setUp(cls):
24+
current_time = datetime.now()
25+
cls.payload = {
26+
"dest": "https://test-shop.myshopify.com",
27+
"aud": cls.api_key,
28+
"exp": timestamp((current_time + timedelta(0, 60))),
29+
"nbf": timestamp(current_time),
30+
"iat": timestamp(current_time),
31+
"jti": "6c992878-dbaf-48d1-bb9d-6d9b59814fd1",
32+
}
33+
34+
@classmethod
35+
def build_auth_header(cls):
36+
mock_session_token = jwt.encode(cls.payload, cls.secret, algorithm="HS256")
37+
return "Bearer {session_token}".format(session_token=mock_session_token)
38+
39+
def test_raises_if_token_authentication_header_is_not_bearer(self):
40+
authorization_header = "Bad auth header"
41+
42+
with self.assertRaises(session_token.TokenAuthenticationError) as cm:
43+
session_token.decode_from_header(
44+
authorization_header, api_key=self.api_key, secret=self.secret, is_extension=True
45+
)
46+
47+
self.assertEqual("The HTTP_AUTHORIZATION_HEADER provided does not contain a Bearer token", str(cm.exception))
48+
49+
def test_raises_extension_is_false_and_invalid_payload(self):
50+
authorization_header = self.build_auth_header()
51+
52+
with self.assertRaises(session_token.SessionTokenError) as cm:
53+
session_token.decode_from_header(
54+
authorization_header, api_key=self.api_key, secret=self.secret, is_extension=False
55+
)
56+
57+
self.assertEqual('Token is missing the "iss" claim', str(cm.exception))

0 commit comments

Comments
 (0)