Repository: python-social-auth/social-core
Pull request #1100 (2025-04-07 17:13:25)

Feature/kick backend
This PR solves https://github.com/python-social-auth/social-core/issues/1096

Add a new backend for Kick integrations.


nijel: Can you please also add documentation to https://github.com/python-social-auth/social-docs?
cdelafuen: Documentation pending on https://github.com/python-social-auth/social-docs/pull/282

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d8d10d5a..e1ed283a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/)
 and this project adheres to [Semantic Versioning](http://semver.org/).
 
+## [4.5.7](https://github.com/python-social-auth/social-core/releases/tag/4.5.7) - 2025-04-04
+
+### Added
+
+- Kick OAuth2 backend implementation
+
 ## [4.5.6](https://github.com/python-social-auth/social-core/releases/tag/4.5.6) - 2025-02-13
 
 ### Changed
diff --git a/social_core/backends/kick.py b/social_core/backends/kick.py
new file mode 100644
index 00000000..d839af8d
--- /dev/null
+++ b/social_core/backends/kick.py
@@ -0,0 +1,53 @@
+"""
+Kick OAuth2 backend, docs at:
+    https://docs.kick.com/getting-started/generating-tokens-oauth2-flow
+"""
+
+from .oauth import BaseOAuth2PKCE
+
+
+class KickOAuth2(BaseOAuth2PKCE):
+    """Kick OAuth2 authentication backend"""
+
+    name = "kick"
+    HOSTNAME = "id.kick.com"
+    API_HOSTNAME = "api.kick.com"
+    AUTHORIZATION_URL = f"https://{HOSTNAME}/oauth/authorize"
+    ACCESS_TOKEN_URL = f"https://{HOSTNAME}/oauth/token"
+    REFRESH_TOKEN_URL = f"https://{HOSTNAME}/oauth/token"
+    REVOKE_TOKEN_URL = f"https://{HOSTNAME}/oauth/revoke"
+    DEFAULT_SCOPE = ["user:read"]
+    SCOPE_SEPARATOR = " "
+    PKCE_DEFAULT_CODE_CHALLENGE_METHOD = "S256"
+    EXTRA_DATA = [
+        ("access_token", "access_token"),
+        ("refresh_token", "refresh_token"),
+        ("expires_in", "expires"),
+        ("token_type", "token_type"),
+        ("scope", "scope"),
+    ]
+
+    def get_user_details(self, response):
+        """Return user details from Kick account"""
+        return {
+            "username": response.get(
+                "name"
+            ),  # API returns 'name' instead of 'username'
+            "email": response.get("email") or "",
+            "fullname": response.get("name") or "",  # Using 'name' as fullname
+            "first_name": "",
+            "last_name": "",
+            "profile_picture": response.get("profile_picture") or "",
+            "user_id": response.get("user_id"),
+        }
+
+    def user_data(self, access_token, *args, **kwargs):
+        """Loads user data from service"""
+        url = f"https://{self.API_HOSTNAME}/public/v1/users"
+        auth_header = {"Authorization": f"Bearer {access_token}"}
+        response = self.get_json(url, headers=auth_header)
+        # The API returns data in a 'data' field with an array of users
+        # For the authenticated user (when no user IDs are specified), we get the first item
+        if response and "data" in response and response["data"]:
+            return response["data"][0]
+        return {}


Test patch:
diff --git a/social_core/tests/backends/test_kick.py b/social_core/tests/backends/test_kick.py
new file mode 100644
index 00000000..02999468
--- /dev/null
+++ b/social_core/tests/backends/test_kick.py
@@ -0,0 +1,38 @@
+import json
+
+from .oauth import OAuth2Test
+
+
+class KickOAuth2Test(OAuth2Test):
+    backend_path = "social_core.backends.kick.KickOAuth2"
+    user_data_url = "https://api.kick.com/public/v1/users"
+    expected_username = "foobar"
+    access_token_body = json.dumps(
+        {
+            "access_token": "foobar",
+            "token_type": "bearer",
+            "refresh_token": "refresh_foobar",
+            "expires_in": 3600,
+            "scope": "user:read",
+        }
+    )
+    # The API returns data in a 'data' field with an array of users
+    user_data_body = json.dumps(
+        {
+            "data": [
+                {
+                    "user_id": 123456,
+                    "name": "foobar",
+                    "email": "foobar@example.com",
+                    "profile_picture": "https://example.com/avatar.jpg",
+                }
+            ],
+            "message": "Success",
+        }
+    )
+
+    def test_login(self):
+        self.do_login()
+
+    def test_partial_pipeline(self):
+        self.do_partial_pipeline()