Repository: goauthentik/authentik

Issue:
The SCIM JSON for User and Group is missing the schema attribute
**Describe the bug**
The SCIM JSON for User and Group is missing the schema attribute

**To Reproduce**
Steps to reproduce the behavior:
Run SCIM sync

**Expected behavior**
JSON containing schemas attribute eg
{
    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
    "userName": "test.user@okta.local",
    "name": {
        "givenName": "Test",
        "familyName": "User"
    },
    "emails": [{
        "primary": true,
        "value": "test.user@okta.local",
        "type": "work"
    }],
    "displayName": "Test User",
    "locale": "en-US",
    "externalId": "00ujl29u0le5T6Aj10h7",
    "groups": [],
    "password": "1mz050nq",
    "active": true
}

**Version and Deployment (please complete the following information):**

-   authentik version: [2023.10.2]
-   Deployment: [docker-compose]




Discussion:

Resolving patch:
diff --git a/authentik/providers/scim/clients/group.py b/authentik/providers/scim/clients/group.py
--- a/authentik/providers/scim/clients/group.py
+++ b/authentik/providers/scim/clients/group.py
@@ -46,7 +46,9 @@ def delete(self, obj: Group):
 
     def to_scim(self, obj: Group) -> SCIMGroupSchema:
         """Convert authentik user into SCIM"""
-        raw_scim_group = {}
+        raw_scim_group = {
+            "schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
+        }
         for mapping in (
             self.provider.property_mappings_group.all().order_by("name").select_subclasses()
         ):
diff --git a/authentik/providers/scim/clients/schema.py b/authentik/providers/scim/clients/schema.py
--- a/authentik/providers/scim/clients/schema.py
+++ b/authentik/providers/scim/clients/schema.py
@@ -15,12 +15,14 @@
 class User(BaseUser):
     """Modified User schema with added externalId field"""
 
+    schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:User",)
     externalId: Optional[str] = None
 
 
 class Group(BaseGroup):
     """Modified Group schema with added externalId field"""
 
+    schemas: tuple[str] = ("urn:ietf:params:scim:schemas:core:2.0:Group",)
     externalId: Optional[str] = None
 
 
diff --git a/authentik/providers/scim/clients/user.py b/authentik/providers/scim/clients/user.py
--- a/authentik/providers/scim/clients/user.py
+++ b/authentik/providers/scim/clients/user.py
@@ -39,7 +39,9 @@ def delete(self, obj: User):
 
     def to_scim(self, obj: User) -> SCIMUserSchema:
         """Convert authentik user into SCIM"""
-        raw_scim_user = {}
+        raw_scim_user = {
+            "schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
+        }
         for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
             if not isinstance(mapping, SCIMMapping):
                 continue


Test patch:
diff --git a/authentik/providers/scim/tests/test_group.py b/authentik/providers/scim/tests/test_group.py
--- a/authentik/providers/scim/tests/test_group.py
+++ b/authentik/providers/scim/tests/test_group.py
@@ -61,7 +61,11 @@ def test_group_create(self, mock: Mocker):
         self.assertEqual(mock.request_history[1].method, "POST")
         self.assertJSONEqual(
             mock.request_history[1].body,
-            {"externalId": str(group.pk), "displayName": group.name},
+            {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
+                "externalId": str(group.pk),
+                "displayName": group.name,
+            },
         )
 
     @Mocker()
@@ -96,7 +100,11 @@ def test_group_create_update(self, mock: Mocker):
             validate(body, loads(schema.read()))
         self.assertEqual(
             body,
-            {"externalId": str(group.pk), "displayName": group.name},
+            {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
+                "externalId": str(group.pk),
+                "displayName": group.name,
+            },
         )
         group.save()
         self.assertEqual(mock.call_count, 4)
@@ -129,7 +137,11 @@ def test_group_create_delete(self, mock: Mocker):
         self.assertEqual(mock.request_history[1].method, "POST")
         self.assertJSONEqual(
             mock.request_history[1].body,
-            {"externalId": str(group.pk), "displayName": group.name},
+            {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
+                "externalId": str(group.pk),
+                "displayName": group.name,
+            },
         )
         group.delete()
         self.assertEqual(mock.call_count, 4)
diff --git a/authentik/providers/scim/tests/test_membership.py b/authentik/providers/scim/tests/test_membership.py
--- a/authentik/providers/scim/tests/test_membership.py
+++ b/authentik/providers/scim/tests/test_membership.py
@@ -89,6 +89,7 @@ def test_member_add(self):
             self.assertJSONEqual(
                 mocker.request_history[3].body,
                 {
+                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                     "emails": [],
                     "active": True,
                     "externalId": user.uid,
@@ -99,7 +100,11 @@ def test_member_add(self):
             )
             self.assertJSONEqual(
                 mocker.request_history[5].body,
-                {"externalId": str(group.pk), "displayName": group.name},
+                {
+                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
+                    "externalId": str(group.pk),
+                    "displayName": group.name,
+                },
             )
 
         with Mocker() as mocker:
@@ -118,6 +123,7 @@ def test_member_add(self):
             self.assertJSONEqual(
                 mocker.request_history[1].body,
                 {
+                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                     "Operations": [
                         {
                             "op": "add",
@@ -125,7 +131,6 @@ def test_member_add(self):
                             "value": [{"value": user_scim_id}],
                         }
                     ],
-                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                 },
             )
 
@@ -174,6 +179,7 @@ def test_member_remove(self):
             self.assertJSONEqual(
                 mocker.request_history[3].body,
                 {
+                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                     "active": True,
                     "displayName": "",
                     "emails": [],
@@ -184,7 +190,11 @@ def test_member_remove(self):
             )
             self.assertJSONEqual(
                 mocker.request_history[5].body,
-                {"externalId": str(group.pk), "displayName": group.name},
+                {
+                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
+                    "externalId": str(group.pk),
+                    "displayName": group.name,
+                },
             )
 
         with Mocker() as mocker:
@@ -203,6 +213,7 @@ def test_member_remove(self):
             self.assertJSONEqual(
                 mocker.request_history[1].body,
                 {
+                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                     "Operations": [
                         {
                             "op": "add",
@@ -210,7 +221,6 @@ def test_member_remove(self):
                             "value": [{"value": user_scim_id}],
                         }
                     ],
-                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                 },
             )
 
@@ -230,6 +240,7 @@ def test_member_remove(self):
             self.assertJSONEqual(
                 mocker.request_history[1].body,
                 {
+                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                     "Operations": [
                         {
                             "op": "remove",
@@ -237,6 +248,5 @@ def test_member_remove(self):
                             "value": [{"value": user_scim_id}],
                         }
                     ],
-                    "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
                 },
             )
diff --git a/authentik/providers/scim/tests/test_user.py b/authentik/providers/scim/tests/test_user.py
--- a/authentik/providers/scim/tests/test_user.py
+++ b/authentik/providers/scim/tests/test_user.py
@@ -66,6 +66,7 @@ def test_user_create(self, mock: Mocker):
         self.assertJSONEqual(
             mock.request_history[1].body,
             {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                 "active": True,
                 "emails": [
                     {
@@ -121,6 +122,7 @@ def test_user_create_update(self, mock: Mocker):
         self.assertEqual(
             body,
             {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                 "active": True,
                 "emails": [
                     {
@@ -173,6 +175,7 @@ def test_user_create_delete(self, mock: Mocker):
         self.assertJSONEqual(
             mock.request_history[1].body,
             {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                 "active": True,
                 "emails": [
                     {
@@ -240,6 +243,7 @@ def test_sync_task(self, mock: Mocker):
         self.assertJSONEqual(
             mock.request_history[1].body,
             {
+                "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
                 "active": True,
                 "emails": [
                     {