Repository: pennersr/django-allauth
Pull request #3722 (2024-04-10 07:51:49)

feat(atlassian): add atlassian oauth2 provider
- **feat(atlassian): add atlassian oauth2 provider**
- **chore(atlassian): add atlassian provider documentation**

# Submitting Pull Requests

## General

 - [x] Make sure you use [semantic commit messages](https://seesparkbox.com/foundry/semantic_commit_messages).
       Examples: `"fix(google): Fixed foobar bug"`, `"feat(accounts): Added foobar feature"`.
 - [x] All Python code must formatted using Black, and clean from pep8 and isort issues.
 - [x] JavaScript code should adhere to [StandardJS](https://standardjs.com).
 - [x] If your changes are significant, please update `ChangeLog.rst`.
 - [x] If your change is substantial, feel free to add yourself to `AUTHORS`.

 ## Provider Specifics

 In case you add a new provider:

- [x] Make sure unit tests are available.
- [x] Add an entry of your provider in `test_settings.py::INSTALLED_APPS` and `docs/installation.rst::INSTALLED_APPS`.
- [x] Add documentation to `docs/providers/<provider name>.rst` and `docs/providers/index.rst` Provider Specifics toctree.
- [x] Add an entry to the list of supported providers over at `docs/overview.rst`.


coveralls: 
[![Coverage Status](https://coveralls.io/builds/66823096/badge)](https://coveralls.io/builds/66823096)

coverage: 95.732% (+0.01%) from 95.72%
when pulling **555cd6cecc97f9e95991bcdfdfa51d978d4aa818 on Ahuahuachi:feature/atlassian-provider**
into **8172ea9fba7d899323ab53069afb80c31fa9f3c3 on pennersr:main**.

pennersr: Question: Isn't Atlassian a fully OpenID Connect compliant provider? Because if it is, we should be able to just use the existing `openid_connect` provider out of the box without adding a new dedicated/hardcoded provider.
Ahuahuachi: AFAIK the OpenID compliant products of Atlassian are the On-Premise products (ex. Jira Server). The Cloud Services (ex. Jira Cloud) are not.

Patch:
diff --git a/AUTHORS b/AUTHORS
index ab41bdf6..9a0d08a7 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -15,6 +15,7 @@ Ahmet Emre Aladağ
 Aiden Lu
 Aldiantoro Nugroho
 Alexander Gaevsky
+Alfredo Altamirano
 Andrean Franc
 Andrei Satsevich
 Andrew Chen Wang
diff --git a/ChangeLog.rst b/ChangeLog.rst
index c8039b24..e084f3ab 100644
--- a/ChangeLog.rst
+++ b/ChangeLog.rst
@@ -6,6 +6,8 @@ Note worthy changes
 
 - Added a dummy provider, useful for testing purposes: ``allauth.socialaccount.providers.dummy``.
 
+- Added a new provider, Atlassian
+
 
 Backwards incompatible changes
 ------------------------------
diff --git a/allauth/socialaccount/providers/atlassian/__init__.py b/allauth/socialaccount/providers/atlassian/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/allauth/socialaccount/providers/atlassian/provider.py b/allauth/socialaccount/providers/atlassian/provider.py
new file mode 100644
index 00000000..566d0ab3
--- /dev/null
+++ b/allauth/socialaccount/providers/atlassian/provider.py
@@ -0,0 +1,38 @@
+from allauth.socialaccount.providers.base import ProviderAccount
+from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
+
+from .views import AtlassianOAuth2Adapter
+
+
+class AtlassianAccount(ProviderAccount):
+    def get_profile_url(self):
+        return self.account.extra_data.get("picture")
+
+
+class AtlassianProvider(OAuth2Provider):
+    id = "atlassian"
+    name = "Atlassian"
+    account_class = AtlassianAccount
+    oauth2_adapter_class = AtlassianOAuth2Adapter
+
+    def extract_uid(self, data):
+        return data["account_id"]
+
+    def extract_common_fields(self, data):
+        return {
+            "email": data.get("email"),
+            "name": data.get("name"),
+            "username": data.get("nickname"),
+            "email_verified": data.get("email_verified"),
+        }
+
+    def get_default_scope(self):
+        return ["read:me"]
+
+    def get_auth_params(self):
+        params = super().get_auth_params()
+        params.update({"audience": "api.atlassian.com", "prompt": "consent"})
+        return params
+
+
+provider_classes = [AtlassianProvider]
diff --git a/allauth/socialaccount/providers/atlassian/urls.py b/allauth/socialaccount/providers/atlassian/urls.py
new file mode 100644
index 00000000..3c540d13
--- /dev/null
+++ b/allauth/socialaccount/providers/atlassian/urls.py
@@ -0,0 +1,6 @@
+from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
+
+from .provider import AtlassianProvider
+
+
+urlpatterns = default_urlpatterns(AtlassianProvider)
diff --git a/allauth/socialaccount/providers/atlassian/views.py b/allauth/socialaccount/providers/atlassian/views.py
new file mode 100644
index 00000000..3acb92e2
--- /dev/null
+++ b/allauth/socialaccount/providers/atlassian/views.py
@@ -0,0 +1,29 @@
+from allauth.socialaccount.adapter import get_adapter
+from allauth.socialaccount.providers.oauth2.views import (
+    OAuth2Adapter,
+    OAuth2CallbackView,
+    OAuth2LoginView,
+)
+
+
+class AtlassianOAuth2Adapter(OAuth2Adapter):
+    provider_id = "atlassian"
+    access_token_url = "https://api.atlassian.com/oauth/token"
+    authorize_url = "https://auth.atlassian.com/authorize"
+    profile_url = "https://api.atlassian.com/me"
+
+    def complete_login(self, request, app, access_token, **kwargs):
+        headers = {
+            "Authorization": f"Bearer {access_token}",
+            "Accept": "application/json",
+        }
+        response = (
+            get_adapter().get_requests_session().get(self.profile_url, headers=headers)
+        )
+        response.raise_for_status()
+        data = response.json()
+        return self.get_provider().sociallogin_from_response(request, data)
+
+
+oauth2_login = OAuth2LoginView.adapter_view(AtlassianOAuth2Adapter)
+oauth2_callback = OAuth2CallbackView.adapter_view(AtlassianOAuth2Adapter)
diff --git a/docs/installation/quickstart.rst b/docs/installation/quickstart.rst
index 0fff0121..95802a2f 100644
--- a/docs/installation/quickstart.rst
+++ b/docs/installation/quickstart.rst
@@ -168,6 +168,7 @@ the ``settings.py`` of your project::
         'allauth.socialaccount.providers.zoom',
         'allauth.socialaccount.providers.okta',
         'allauth.socialaccount.providers.feishu',
+        "allauth.socialaccount.providers.atlassian",
         ...
     ]
 
diff --git a/docs/socialaccount/providers/atlassian.rst b/docs/socialaccount/providers/atlassian.rst
new file mode 100644
index 00000000..cb04fb2a
--- /dev/null
+++ b/docs/socialaccount/providers/atlassian.rst
@@ -0,0 +1,56 @@
+Atlassian
+---------
+
+Atlassian OAuth 2.0 (3LO) apps provider.
+
+More info:
+    https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/
+
+
+Enabling OAuth 2.0 (3LO)
+************************
+Before you can implement OAuth 2.0 (3LO) for your app, you need to enable it for your app using the developer console.
+
+Atlassian developer console
+    https://developer.atlassian.com/console/myapps/
+
+1. From any page on developer.atlassian.com, select your profile icon in the top-right corner, and from the dropdown, select Developer console.
+2. Select your app from the list (or create one if you don't already have one).
+3. Select Authorization in the left menu.
+4. Next to OAuth 2.0 (3LO), select Configure.
+5. Enter the Callback URL. Ex. ``http://127.0.0.1:8000/accounts/atlassian/login/callback/``
+6. Click Save changes.
+
+Note, if you haven't already added an API to your app, you should do this now:
+
+1. Select Permissions in the left menu.
+2. Next to the API you want to add, select Add.
+
+
+Django setup
+************
+The app credentials are configured for your Django installation via the admin
+interface. Create a new socialapp through ``/admin/socialaccount/socialapp/``.
+
+Fill in the form as follows:
+
+* Provider, "Atlassian"
+* Name, your pick, suggest "Atlassian"
+* Client id, is called "Client ID" by Atlassian
+* Secret key, is called "Secret" by Atlassian
+* Key, is not needed, leave blank.
+
+Optionally, you can specify the scope to use as follows:
+
+.. code-block:: python
+
+    SOCIALACCOUNT_PROVIDERS = {
+        'atlassian': {
+            'SCOPE': [
+                'read:me',
+                'write:jira-work',
+            ],
+        }
+    }
+
+.. note:: By default (if you do not specify ``SCOPE``), ``read:me`` scope is requested.
diff --git a/docs/socialaccount/providers/index.rst b/docs/socialaccount/providers/index.rst
index 862e5a5d..19ea2bbd 100644
--- a/docs/socialaccount/providers/index.rst
+++ b/docs/socialaccount/providers/index.rst
@@ -40,6 +40,7 @@ Provider Specifics
    amazon
    angellist
    apple
+   atlassian
    auth0
    authentiq
    baidu


Test patch:
diff --git a/allauth/socialaccount/providers/atlassian/tests.py b/allauth/socialaccount/providers/atlassian/tests.py
new file mode 100644
index 00000000..53d15b36
--- /dev/null
+++ b/allauth/socialaccount/providers/atlassian/tests.py
@@ -0,0 +1,30 @@
+from allauth.socialaccount.tests import OAuth2TestsMixin
+from allauth.tests import MockedResponse, TestCase
+
+from .provider import AtlassianProvider
+
+
+class AtlassianTests(OAuth2TestsMixin, TestCase):
+    provider_id = AtlassianProvider.id
+
+    def get_mocked_response(self):
+        response_data = """
+        {
+            "account_type": "atlassian",
+            "account_id": "112233aa-bb11-cc22-33dd-445566abcabc",
+            "email": "mia@example.com",
+            "email_verified": true,
+            "name": "Mia Krystof",
+            "picture": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/112233aa-bb11-cc22-33dd-445566abcabc/1234abcd-9876-54aa-33aa-1234dfsade9487ds",
+            "account_status": "active",
+            "nickname": "mkrystof",
+            "zoneinfo": "Australia/Sydney",
+            "locale": "en-US",
+            "extended_profile": {
+                "job_title": "Designer",
+                "organization": "mia@example.com",
+                "department": "Design team",
+                "location": "Sydney"
+            }
+        }"""
+        return MockedResponse(200, response_data)
diff --git a/test_settings.py b/test_settings.py
index 87517141..b2a71a16 100644
--- a/test_settings.py
+++ b/test_settings.py
@@ -82,6 +82,7 @@ INSTALLED_APPS = (
     "allauth.socialaccount.providers.angellist",
     "allauth.socialaccount.providers.apple",
     "allauth.socialaccount.providers.asana",
+    "allauth.socialaccount.providers.atlassian",
     "allauth.socialaccount.providers.auth0",
     "allauth.socialaccount.providers.authentiq",
     "allauth.socialaccount.providers.baidu",