From fee0fe08b346092ed12e62196eec8e64d3a6003d Mon Sep 17 00:00:00 2001 From: Sergey Dyudin <37060845+SergeyDyudin@users.noreply.github.com> Date: Fri, 10 Feb 2023 04:07:28 +0300 Subject: [PATCH] feat: Added ability to use Client Credentials Grant (#39) * feat:Added getting token using client credentials - Added getting access token using client credentials grant. Use get_oauth_token() without params - Minor logic refactoring * fix:Fixed linter issue --- README.md | 16 ++++++++-- src/casdoor/async_main.py | 47 ++++++++++++++++++----------- src/casdoor/main.py | 56 ++++++++++++++++++++++------------- src/tests/test_async_oauth.py | 19 ++++++++++-- src/tests/test_oauth.py | 19 ++++++++++-- 5 files changed, 113 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 7eb569a..9f154a3 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ casdoor-python-sdk support basic user operations, like: - `enforce(self, permission_model_name: str, sub: str, obj: str, act: str)`, check permission from model -## Also. Resource Owner Password Credentials Grant +## Resource Owner Password Credentials Grant If your application doesn't have a frontend that redirects users to Casdoor and you have Password Credentials Grant enabled, then you may get access token like this: @@ -125,4 +125,16 @@ access_token = sdk.get_oauth_token(username=username, password=password) decoded_msg = sdk.parse_jwt_token(access_token) ``` -`decoded_msg` is the JSON data decoded from the `access_token`, which contains user info and other useful stuff. \ No newline at end of file +`decoded_msg` is the JSON data decoded from the `access_token`, which contains user info and other useful stuff. + +## Client Credentials Grant + +You can also use Client Credentials Grant when your application does not have a frontend. +It is important to note that the AccessToken obtained in this way differs from other in that it corresponds to the application rather than to the user. + +```python +access_token = sdk.get_oauth_token() +decoded_msg = sdk.parse_jwt_token(access_token) +``` + +`decoded_msg` is the JSON data decoded from the `access_token`. \ No newline at end of file diff --git a/src/casdoor/async_main.py b/src/casdoor/async_main.py index dbb7fa0..70e507b 100644 --- a/src/casdoor/async_main.py +++ b/src/casdoor/async_main.py @@ -88,6 +88,7 @@ async def get_oauth_token( """ Request the Casdoor server to get access_token. Must be set code or username and password for grant type. + If nothing is set then client credentials grant will be used. :param code: the code that sent from Casdoor using redirect url back to your server. @@ -95,12 +96,7 @@ async def get_oauth_token( :param password: username password :return: access token: str """ - payload = self._get_payload_for_access_token_request( - code=code, - username=username, - password=password - ) - response = await self._oath_token_request(payload) + response = await self.oauth_token_request(code, username, password) access_token = response.get("access_token") return access_token @@ -122,8 +118,7 @@ def _get_payload_for_access_token_request( password=password ) else: - raise ValueError("Attributes for some grant type must be set" - "(code or username and password)") + return self.__get_payload_for_client_credentials() def __get_payload_for_authorization_code(self, code: str) -> dict: """ @@ -153,24 +148,42 @@ def __get_payload_for_password_credentials( "password": password } - async def oauth_token_request(self, code: str) -> dict: + def __get_payload_for_client_credentials(self) -> dict: + """ + Return payload for auth request with client credentials. + """ + return { + "grant_type": "client_credentials", + "client_id": self.client_id, + "client_secret": self.client_secret, + } + + async def oauth_token_request( + self, + code: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None + ) -> dict: """ Request the Casdoor server to get access_token. + Must be set code or username and password for grant type. + If nothing is set then client credentials grant will be used. Returns full response as dict. :param code: the code that sent from Casdoor using redirect url back to your server. + :param username: Casdoor username + :param password: username password :return: Response from Casdoor """ - params = { - "grant_type": self.grant_type, - "client_id": self.client_id, - "client_secret": self.client_secret, - "code": code, - } - return await self._oath_token_request(payload=params) + params = self._get_payload_for_access_token_request( + code=code, + username=username, + password=password + ) + return await self._oauth_token_request(payload=params) - async def _oath_token_request(self, payload: dict) -> dict: + async def _oauth_token_request(self, payload: dict) -> dict: """ Request the Casdoor server to get access_token. diff --git a/src/casdoor/main.py b/src/casdoor/main.py index de2986d..54403d4 100644 --- a/src/casdoor/main.py +++ b/src/casdoor/main.py @@ -80,8 +80,9 @@ def get_oauth_token( password: Optional[str] = None ) -> str: """ - Request the Casdoor server to get access_token. Must be set code or - username and password for grant type. + Request the Casdoor server to get access_token. + Must be set code or username and password for grant type. + If nothing is set then client credentials grant will be used. :param code: the code that sent from Casdoor using redirect url back to your server. @@ -89,12 +90,7 @@ def get_oauth_token( :param password: username password :return: access_token: str """ - payload = self._get_payload_for_access_token_request( - code=code, - username=username, - password=password - ) - response = self._oath_token_request(payload) + response = self.oauth_token_request(code, username, password) access_token = response.json().get("access_token") return access_token @@ -116,8 +112,7 @@ def _get_payload_for_access_token_request( password=password ) else: - raise ValueError("Attributes for some grant type must be set" - "(code or username and password)") + return self.__get_payload_for_client_credentials() def __get_payload_for_authorization_code(self, code: str) -> dict: """ @@ -147,23 +142,42 @@ def __get_payload_for_password_credentials( "password": password } - def oauth_token_request(self, code: str) -> requests.Response: + def __get_payload_for_client_credentials(self) -> dict: """ - Request the Casdoor server to get access_token. - - :param code: the code that sent from Casdoor using redirect - url back to your server. - :return: Response from Casdoor + Return payload for auth request with client credentials. """ - params = { - "grant_type": self.grant_type, + return { + "grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret, - "code": code, } - return self._oath_token_request(payload=params) - def _oath_token_request(self, payload: dict) -> requests.Response: + def oauth_token_request( + self, + code: Optional[str] = None, + username: Optional[str] = None, + password: Optional[str] = None + ) -> requests.Response: + """ + Request the Casdoor server to get access_token. + Must be set code or username and password for grant type. + If nothing is set then client credentials grant will be used. + Returns full response as dict. + + :param code: the code that sent from Casdoor using redirect url + back to your server. + :param username: Casdoor username + :param password: username password + :return: Response from Casdoor + """ + params = self._get_payload_for_access_token_request( + code=code, + username=username, + password=password + ) + return self._oauth_token_request(payload=params) + + def _oauth_token_request(self, payload: dict) -> requests.Response: """ Request the Casdoor server to get access_token. diff --git a/src/tests/test_async_oauth.py b/src/tests/test_async_oauth.py index fb959fb..f0e2d5f 100644 --- a/src/tests/test_async_oauth.py +++ b/src/tests/test_async_oauth.py @@ -47,7 +47,7 @@ def get_sdk(): ) return sdk - async def test__oath_token_request(self): + async def test__oauth_token_request(self): sdk = self.get_sdk() data = { "grant_type": sdk.grant_type, @@ -55,7 +55,7 @@ async def test__oath_token_request(self): "client_secret": sdk.client_secret, "code": self.code, } - response = await sdk._oath_token_request(payload=data) + response = await sdk._oauth_token_request(payload=data) self.assertIsInstance(response, dict) async def test__get_payload_for_authorization_code(self): @@ -73,6 +73,11 @@ async def test__get_payload_for_password_credentials(self): ) self.assertEqual("password", result.get("grant_type")) + async def test__get_payload_for_client_credentials(self): + sdk = self.get_sdk() + result = sdk._AsyncCasdoorSDK__get_payload_for_client_credentials() # noqa: It's private method + self.assertEqual("client_credentials", result.get("grant_type")) + async def test__get_payload_for_access_token_request_with_code(self): sdk = self.get_sdk() result = sdk._get_payload_for_access_token_request(code="test") @@ -86,6 +91,11 @@ async def test__get_payload_for_access_token_request_with_cred(self): ) self.assertEqual("password", result.get("grant_type")) + async def test_get_payload_for_access_token_request_with_client_cred(self): + sdk = self.get_sdk() + result = sdk._get_payload_for_access_token_request() + self.assertEqual("client_credentials", result.get("grant_type")) + async def test_get_oauth_token_with_password(self): sdk = self.get_sdk() access_token = await sdk.get_oauth_token( @@ -94,6 +104,11 @@ async def test_get_oauth_token_with_password(self): ) self.assertIsInstance(access_token, str) + async def test_get_oauth_token_with_client_cred(self): + sdk = self.get_sdk() + access_token = await sdk.get_oauth_token() + self.assertIsInstance(access_token, str) + async def test_get_oauth_token(self): sdk = self.get_sdk() access_token = await sdk.get_oauth_token(code=self.code) diff --git a/src/tests/test_oauth.py b/src/tests/test_oauth.py index 9a8db7a..05a08b7 100644 --- a/src/tests/test_oauth.py +++ b/src/tests/test_oauth.py @@ -49,7 +49,7 @@ def get_sdk(): ) return sdk - def test__oath_token_request(self): + def test__oauth_token_request(self): sdk = self.get_sdk() data = { "grant_type": sdk.grant_type, @@ -57,7 +57,7 @@ def test__oath_token_request(self): "client_secret": sdk.client_secret, "code": self.code, } - response = sdk._oath_token_request(payload=data) + response = sdk._oauth_token_request(payload=data) self.assertIsInstance(response, dict) def test__get_payload_for_authorization_code(self): @@ -67,6 +67,11 @@ def test__get_payload_for_authorization_code(self): ) self.assertEqual("authorization_code", result.get("grant_type")) + def test__get_payload_for_client_credentials(self): + sdk = self.get_sdk() + result = sdk._CasdoorSDK__get_payload_for_client_credentials() # noqa: It's private method + self.assertEqual("client_credentials", result.get("grant_type")) + def test__get_payload_for_password_credentials(self): sdk = self.get_sdk() result = sdk._CasdoorSDK__get_payload_for_password_credentials( # noqa: It's private method @@ -80,6 +85,11 @@ def test__get_payload_for_access_token_request_with_code(self): result = sdk._get_payload_for_access_token_request(code="test") self.assertEqual("authorization_code", result.get("grant_type")) + def test__get_payload_for_access_token_request_with_client_cred(self): + sdk = self.get_sdk() + result = sdk._get_payload_for_access_token_request() + self.assertEqual("client_credentials", result.get("grant_type")) + def test__get_payload_for_access_token_request_with_cred(self): sdk = self.get_sdk() result = sdk._get_payload_for_access_token_request( @@ -88,6 +98,11 @@ def test__get_payload_for_access_token_request_with_cred(self): ) self.assertEqual("password", result.get("grant_type")) + def test_get_oauth_token_with_client_cred(self): + sdk = self.get_sdk() + access_token = sdk.get_oauth_token() + self.assertIsInstance(access_token, str) + def test_get_oauth_token_with_code(self): sdk = self.get_sdk() access_token = sdk.get_oauth_token(code=self.code)