Repository: auth0/auth0-python
Pull request #682 (2025-03-29 03:01:21)

feat: Federated Connections Support
### Changes

- Added support for Federated Login for Authentication and Management APIs.

   - Extended the [/oauth/token (https://oktawiki.atlassian.net/wiki/spaces/AFG1/pages/3178596704/RFC+Federated+Connections+Access+Tokens+-+EA) to support federated login.
   - Two new endpoints added to [Users.py](https://github.com/auth0/auth0-python/blob/master/auth0/management/users.py) i.e.
       - Get a List of All Token Sets
       - Revoking a token set

### References

- [Internal Reference](https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-initiated-backchannel-authentication-flow/user-authentication-with-ciba)


### Testing

- [x] This change adds test coverage

- [x] This change has been tested on the latest version of the platform/language or why not

### Contributor Checklist

- [x] I agree to adhere to the [Auth0 General Contribution Guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md).
- [x] I agree to uphold the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md).


Patch:
diff --git a/auth0/authentication/get_token.py b/auth0/authentication/get_token.py
index 75b3520..6d71c08 100644
--- a/auth0/authentication/get_token.py
+++ b/auth0/authentication/get_token.py
@@ -276,4 +276,39 @@ class GetToken(AuthenticationBase):
                 "auth_req_id": auth_req_id,
                 "grant_type": grant_type,
             },
+        )
+    
+    def access_token_for_connection(
+        self,
+        subject_token_type: str, 
+        subject_token: str,
+        requested_token_type: str,
+        connection: str | None = None,
+        grant_type: str = "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token"
+    ) -> Any:
+        """Calls /oauth/token endpoint with federated-connection-access-token grant type
+
+        Args:
+            subject_token_type (str): String containing the type of token.
+
+            subject_token (str): String containing the value of subject_token_type.
+
+            requested_token_type (str): String containing the type of rquested token.
+
+            connection (str, optional): Denotes the name of a social identity provider configured to your application         
+
+        Returns:
+            access_token, scope, issued_token_type, token_type
+        """
+
+        return self.authenticated_post(
+            f"{self.protocol}://{self.domain}/oauth/token",
+            data={
+                "client_id": self.client_id,
+                "grant_type": grant_type,
+                "subject_token_type": subject_token_type,                
+                "subject_token": subject_token,
+                "requested_token_type": requested_token_type,
+                "connection": connection,
+            },
         )
\ No newline at end of file
diff --git a/auth0/management/users.py b/auth0/management/users.py
index 77a5e51..2fd9a46 100644
--- a/auth0/management/users.py
+++ b/auth0/management/users.py
@@ -537,4 +537,47 @@ class Users:
         """
 
         url = self._url(f"{user_id}/authentication-methods/{authentication_method_id}")
-        return self.client.delete(url)
\ No newline at end of file
+        return self.client.delete(url)
+
+    def list_tokensets(
+         self, id: str, page: int = 0, per_page: int = 25, include_totals: bool = True
+     ):
+         """List all the tokenset(s) associated to the user.
+ 
+         Args:
+             id (str): The user's id.
+ 
+             page (int, optional): The result's page number (zero based). By default,
+                retrieves the first page of results.
+ 
+             per_page (int, optional): The amount of entries per page. By default,
+                retrieves 25 results per page.
+ 
+             include_totals (bool, optional): True if the query summary is
+                 to be included in the result, False otherwise. Defaults to True.
+ 
+         See https://auth0.com/docs/api/management/v2#!/Users/get_tokensets
+         """
+ 
+         params = {
+             "per_page": per_page,
+             "page": page,
+             "include_totals": str(include_totals).lower(),
+         }
+         url = self._url(f"{id}/federated-connections-tokensets")
+         return self.client.get(url, params=params)
+ 
+    def delete_tokenset_by_id(
+         self, user_id: str, tokenset_id: str
+     ) -> Any:
+         """Deletes an tokenset by ID.
+ 
+         Args:
+             user_id (str):  The user_id to delete an authentication method by ID for.
+             tokenset_id (str):  The tokenset_id to delete an tokenset by ID for.
+ 
+         See: https://auth0.com/docs/api/management/v2#!/Users/delete_tokenset_by_id
+         """
+ 
+         url = self._url(f"{user_id}/federated-connections-tokensets/{tokenset_id}")
+         return self.client.delete(url)
\ No newline at end of file


Test patch:
diff --git a/auth0/test/authentication/test_get_token.py b/auth0/test/authentication/test_get_token.py
index 4e71758..ac152dd 100644
--- a/auth0/test/authentication/test_get_token.py
+++ b/auth0/test/authentication/test_get_token.py
@@ -334,4 +334,34 @@ class TestGetToken(unittest.TestCase):
                 "auth_req_id": "reqid",
                 "grant_type": "urn:openid:params:grant-type:ciba",
             },
+        )
+        
+    @mock.patch("auth0.rest.RestClient.post")
+    def test_connection_login(self, mock_post):
+        g = GetToken("my.domain.com", "cid", client_secret="csec")
+
+        g.access_token_for_connection(
+            grant_type="urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
+            subject_token_type="urn:ietf:params:oauth:token-type:refresh_token",
+            subject_token="refid",
+            requested_token_type="http://auth0.com/oauth/token-type/federated-connection-access-token",
+            connection="google-oauth2"
+        )
+
+        args, kwargs = mock_post.call_args
+
+        print(kwargs["data"])
+
+        self.assertEqual(args[0], "https://my.domain.com/oauth/token")
+        self.assertEqual(
+            kwargs["data"],
+            {
+                "grant_type": "urn:auth0:params:oauth:grant-type:token-exchange:federated-connection-access-token",
+                "client_id": "cid",
+                "client_secret": "csec",
+                "subject_token_type": "urn:ietf:params:oauth:token-type:refresh_token",
+                "subject_token": "refid",
+                "requested_token_type": "http://auth0.com/oauth/token-type/federated-connection-access-token",
+                "connection": "google-oauth2"
+            },
         )
\ No newline at end of file
diff --git a/auth0/test/management/test_users.py b/auth0/test/management/test_users.py
index 28a333c..9cab65f 100644
--- a/auth0/test/management/test_users.py
+++ b/auth0/test/management/test_users.py
@@ -402,4 +402,37 @@ class TestUsers(unittest.TestCase):
 
         mock_instance.delete.assert_called_with(
             "https://domain/api/v2/users/user_id/authentication-methods/authentication_method_id"
+        )
+
+    @mock.patch("auth0.management.users.RestClient")
+    def test_list_tokensets(self, mock_rc):
+        mock_instance = mock_rc.return_value
+ 
+        u = Users(domain="domain", token="jwttoken")
+        u.list_tokensets("an-id")
+ 
+        args, kwargs = mock_instance.get.call_args
+        self.assertEqual("https://domain/api/v2/users/an-id/federated-connections-tokensets", args[0])
+        self.assertEqual(
+             kwargs["params"], {"per_page": 25, "page": 0, "include_totals": "true"}
+        )
+ 
+        u.list_tokensets(id="an-id", page=1, per_page=50, include_totals=False)
+ 
+        args, kwargs = mock_instance.get.call_args
+ 
+        self.assertEqual("https://domain/api/v2/users/an-id/federated-connections-tokensets", args[0])
+        self.assertEqual(
+            kwargs["params"], {"per_page": 50, "page": 1, "include_totals": "false"}
+        )
+ 
+    @mock.patch("auth0.management.users.RestClient")
+    def test_delete_tokenset_by_id(self, mock_rc):
+        mock_instance = mock_rc.return_value
+ 
+        u = Users(domain="domain", token="jwttoken")
+        u.delete_tokenset_by_id("user_id", "tokenset_id")
+ 
+        mock_instance.delete.assert_called_with(
+            "https://domain/api/v2/users/user_id/federated-connections-tokensets/tokenset_id"
         )
\ No newline at end of file