Repository: saleor/saleor

Issue:
Bug: Missing "app id" in app extension token
### What are you trying to achieve?

App (via app-sdk) is checking `app` field in user JWT token (sent from dashboard, via postmessage to appbridge) and compares it to registered app ID (that app persists). If app ID is different, app is rejecting the call.

However, there are also extensions that have their own token (they can have different permissions). **This token lacks `app` field**. 

For app, extension is its part, so its expected to have app ID in the token as well. Otherwise, app would require to persist all registered extensions to work, which is quite hard to implement.

### Steps to reproduce the problem

## App token payload

```json
{
  "iat": 1679048767,
  "owner": "saleor",
  "iss": "https://automation-dashboard.staging.saleor.cloud/graphql/",
  "exp": 1679135167,
  "token": "M2irFmzVASR3",
  "email": "testers+dashboard@saleor.io",
  "type": "thirdparty",
  "user_id": "VXNlcjoxMDMz",
  "is_staff": true,
  "app": "QXBwOjY0",
  "permissions": [
    "MANAGE_PRODUCTS"
  ],
  "user_permissions": [
    "MANAGE_GIFT_CARD",
    "MANAGE_MENUS",
    "MANAGE_PAGES",
    "MANAGE_PAGE_TYPES_AND_ATTRIBUTES",
    "MANAGE_PLUGINS",
    "MANAGE_TAXES",
    "MANAGE_USERS",
    "MANAGE_CHECKOUTS",
    "MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES",
    "MANAGE_TRANSLATIONS",
    "MANAGE_APPS",
    "MANAGE_OBSERVABILITY",
    "MANAGE_STAFF",
    "HANDLE_TAXES",
    "MANAGE_CHANNELS",
    "HANDLE_CHECKOUTS",
    "MANAGE_SETTINGS",
    "HANDLE_PAYMENTS",
    "MANAGE_ORDERS",
    "MANAGE_PRODUCTS",
    "MANAGE_SHIPPING",
    "MANAGE_DISCOUNTS",
    "IMPERSONATE_USER"
  ]
}
```


## Extension token payload

```json
  "owner": "saleor",
  "iss": "https://automation-dashboard.staging.saleor.cloud/graphql/",
  "exp": 1679137338,
  "token": "M2irFmzVASR3",
  "email": "testers+dashboard@saleor.io",
  "type": "thirdparty",
  "user_id": "VXNlcjoxMDMz",
  "is_staff": true,
  "app_extension": "QXBwRXh0ZW5zaW9uOjQy",
  "permissions": [],
  "user_permissions": [
    "MANAGE_GIFT_CARD",
    "MANAGE_MENUS",
    "MANAGE_PAGES",
    "MANAGE_PAGE_TYPES_AND_ATTRIBUTES",
    "MANAGE_PLUGINS",
    "MANAGE_TAXES",
    "MANAGE_USERS",
    "MANAGE_CHECKOUTS",
    "MANAGE_PRODUCT_TYPES_AND_ATTRIBUTES",
    "MANAGE_TRANSLATIONS",
    "MANAGE_APPS",
    "MANAGE_OBSERVABILITY",
    "MANAGE_STAFF",
    "HANDLE_TAXES",
    "MANAGE_CHANNELS",
    "HANDLE_CHECKOUTS",
    "MANAGE_SETTINGS",
    "HANDLE_PAYMENTS",
    "MANAGE_ORDERS",
    "MANAGE_PRODUCTS",
    "MANAGE_SHIPPING",
    "MANAGE_DISCOUNTS",
    "IMPERSONATE_USER"
  ]
}
```

### What did you expect to happen?

Extension token should contain `app` field

### Logs

_No response_

### Environment

Saleor version: …
OS and version: …



Discussion:

Resolving patch:
diff --git a/saleor/core/jwt.py b/saleor/core/jwt.py
--- a/saleor/core/jwt.py
+++ b/saleor/core/jwt.py
@@ -20,6 +20,7 @@
 JWT_THIRDPARTY_ACCESS_TYPE = "thirdparty"
 JWT_REFRESH_TOKEN_COOKIE_NAME = "refreshToken"
 
+APP_KEY_FIELD = "app"
 PERMISSIONS_FIELD = "permissions"
 USER_PERMISSION_FIELD = "user_permissions"
 JWT_SALEOR_OWNER_NAME = "saleor"
@@ -160,22 +161,22 @@ def get_user_from_access_payload(payload: dict, request=None) -> Optional[User]:
 def _create_access_token_for_third_party_actions(
     permissions: Iterable["Permission"],
     user: "User",
-    type: str,
-    object_id: int,
-    object_payload_key: str,
-    audience: Optional[str],
+    app: "App",
+    extra: Optional[Dict[str, Any]] = None,
 ):
     app_permission_enums = get_permission_names(permissions)
 
     permissions = user.effective_permissions
     user_permission_enums = get_permission_names(permissions)
     additional_payload = {
-        object_payload_key: graphene.Node.to_global_id(type, object_id),
+        APP_KEY_FIELD: graphene.Node.to_global_id("App", app.id),
         PERMISSIONS_FIELD: list(app_permission_enums & user_permission_enums),
         USER_PERMISSION_FIELD: list(user_permission_enums),
     }
-    if audience:
-        additional_payload["aud"] = audience
+    if app.audience:
+        additional_payload["aud"] = app.audience
+    if extra:
+        additional_payload.update(extra)
 
     payload = jwt_user_payload(
         user,
@@ -196,12 +197,7 @@ def create_access_token_for_app(app: "App", user: "User"):
     """
     app_permissions = app.permissions.all()
     return _create_access_token_for_third_party_actions(
-        permissions=app_permissions,
-        user=user,
-        type="App",
-        object_id=app.id,
-        object_payload_key="app",
-        audience=app.audience,
+        permissions=app_permissions, user=user, app=app
     )
 
 
@@ -211,11 +207,10 @@ def create_access_token_for_app_extension(
     user: "User",
     app: "App",
 ):
+    app_extension_id = graphene.Node.to_global_id("AppExtension", app_extension.id)
     return _create_access_token_for_third_party_actions(
         permissions=permissions,
         user=user,
-        type="AppExtension",
-        object_id=app_extension.id,
-        object_payload_key="app_extension",
-        audience=app.audience,
+        app=app,
+        extra={"app_extension": app_extension_id},
     )


Test patch:
diff --git a/saleor/core/tests/test_jwt.py b/saleor/core/tests/test_jwt.py
--- a/saleor/core/tests/test_jwt.py
+++ b/saleor/core/tests/test_jwt.py
@@ -3,10 +3,44 @@
 from cryptography.hazmat.primitives import serialization
 from django.urls import reverse
 
-from ..jwt import create_access_token_for_app_extension, jwt_decode, jwt_encode
+from ..jwt import (
+    create_access_token_for_app,
+    create_access_token_for_app_extension,
+    jwt_decode,
+    jwt_encode,
+)
 from ..utils import build_absolute_uri
 
 
+def test_create_access_token_for_app(
+    app,
+    staff_user,
+    permission_manage_apps,
+    permission_manage_products,
+    site_settings,
+):
+    # given
+    staff_user.user_permissions.set(
+        [permission_manage_apps, permission_manage_products]
+    )
+    app.permissions.add(permission_manage_products)
+    audience = f"https://{site_settings.site.domain}.com/app-123"
+    app.audience = audience
+    app.save()
+
+    # when
+    access_token = create_access_token_for_app(app=app, user=staff_user)
+
+    # then
+    decoded_token = jwt_decode(access_token, verify_expiration=False, verify_aud=False)
+    _, decode_app_id = graphene.Node.from_global_id(decoded_token["app"])
+    assert decoded_token["permissions"] == ["MANAGE_PRODUCTS"]
+    assert set(decoded_token["user_permissions"]) == {"MANAGE_APPS", "MANAGE_PRODUCTS"}
+    assert int(decode_app_id) == app.id
+    assert decoded_token["aud"] == audience
+    assert decoded_token["iss"] == build_absolute_uri(reverse("api"))
+
+
 def test_create_access_token_for_app_extension_staff_user_with_more_permissions(
     app_with_extensions,
     staff_user,
@@ -41,10 +75,12 @@ def test_create_access_token_for_app_extension_staff_user_with_more_permissions(
     _, decode_extension_id = graphene.Node.from_global_id(
         decoded_token["app_extension"]
     )
+    _, decode_app_id = graphene.Node.from_global_id(decoded_token["app"])
     assert set(decoded_token["user_permissions"]) == set(
         ["MANAGE_CHANNELS", "MANAGE_APPS", "MANAGE_PRODUCTS"]
     )
     assert int(decode_extension_id) == extension.id
+    assert int(decode_app_id) == app.id
     assert decoded_token["aud"] == audience
     assert decoded_token["iss"] == build_absolute_uri(reverse("api"))
 
@@ -84,8 +120,10 @@ def test_create_access_token_for_app_extension_with_more_permissions(
     _, decode_extension_id = graphene.Node.from_global_id(
         decoded_token["app_extension"]
     )
+    _, decode_app_id = graphene.Node.from_global_id(decoded_token["app"])
     assert decoded_token["user_permissions"] == ["MANAGE_PRODUCTS"]
     assert int(decode_extension_id) == extension.id
+    assert int(decode_app_id) == app.id
     assert decoded_token["aud"] == audience