Skip to content

Commit 27c18c1

Browse files
authored
Add support for auth management key (#624)
1 parent a8ddc44 commit 27c18c1

File tree

4 files changed

+203
-4
lines changed

4 files changed

+203
-4
lines changed

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,23 @@ pip install descope[Flask]
2626
A Descope `Project ID` is required to initialize the SDK. Find it on the
2727
[project page in the Descope Console](https://app.descope.com/settings/project).
2828

29+
**Note:** Authentication APIs public access can be disabled via the Descope console.
30+
If disabled, it's still possible to use the authentication API by providing a management key with
31+
the appropriate access (`Authentication` / `Full Access`).
32+
If not provided directly, this value is retrieved from the `DESCOPE_AUTH_MANAGEMENT_KEY` environment variable instead.
33+
If neither values are set then any disabled authentication methods API calls will fail.
34+
2935
```python
3036
from descope import DescopeClient
3137

32-
# Initialized after setting the DESCOPE_PROJECT_ID env var
38+
# Initialized after setting the DESCOPE_PROJECT_ID and DESCOPE_AUTH_MANAGEMENT_KEY env vars
3339
descope_client = DescopeClient()
3440

3541
# ** Or directly **
36-
descope_client = DescopeClient(project_id="<Project ID>")
42+
descope_client = DescopeClient(
43+
project_id="<Project ID>"
44+
auth_management_key="<Auth Managemet Key>
45+
)
3746
```
3847

3948
## Authentication Functions
@@ -1178,7 +1187,7 @@ type doc
11781187
permission can_create: owner | parent.owner
11791188
permission can_edit: editor | can_create
11801189
permission can_view: viewer | can_edit
1181-
```
1190+
```
11821191
11831192
Descope SDK allows you to fully manage the schema and relations as well as perform simple (and not so simple) checks regarding the existence of relations.
11841193

descope/auth.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def __init__(
7373
management_key: str | None = None,
7474
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
7575
jwt_validation_leeway: int = 5,
76+
auth_management_key: str | None = None,
7677
):
7778
self.lock_public_keys = Lock()
7879
# validate project id
@@ -95,6 +96,9 @@ def __init__(
9596
self.base_url = self.base_url_for_project_id(self.project_id)
9697
self.timeout_seconds = timeout_seconds
9798
self.management_key = management_key or os.getenv("DESCOPE_MANAGEMENT_KEY")
99+
self.auth_management_key = auth_management_key or os.getenv(
100+
"DESCOPE_AUTH_MANAGEMENT_KEY"
101+
)
98102

99103
public_key = public_key or os.getenv("DESCOPE_PUBLIC_KEY")
100104
with self.lock_public_keys:
@@ -564,6 +568,8 @@ def _get_default_headers(self, pswd: str | None = None):
564568
bearer = self.project_id
565569
if pswd:
566570
bearer = f"{self.project_id}:{pswd}"
571+
if self.auth_management_key:
572+
bearer = f"{bearer}:{self.auth_management_key}"
567573
headers["Authorization"] = f"Bearer {bearer}"
568574
return headers
569575

descope/descope_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def __init__(
3030
management_key: str | None = None,
3131
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
3232
jwt_validation_leeway: int = 5,
33+
auth_management_key: str | None = None,
3334
):
3435
auth = Auth(
3536
project_id,
@@ -38,6 +39,7 @@ def __init__(
3839
management_key,
3940
timeout_seconds,
4041
jwt_validation_leeway,
42+
auth_management_key,
4143
)
4244
self._auth = auth
4345
self._mgmt = MGMT(auth)

tests/test_descope_client.py

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414
DescopeClient,
1515
RateLimitException,
1616
)
17-
from descope.common import DEFAULT_TIMEOUT_SECONDS, SESSION_TOKEN_NAME, EndpointsV1
17+
from descope.common import (
18+
DEFAULT_TIMEOUT_SECONDS,
19+
SESSION_TOKEN_NAME,
20+
DeliveryMethod,
21+
EndpointsV1,
22+
)
1823

1924
from . import common
2025

@@ -841,6 +846,183 @@ def test_select_tenant(self):
841846
timeout=DEFAULT_TIMEOUT_SECONDS,
842847
)
843848

849+
def test_auth_management_key_with_functions(self):
850+
"""Test auth_management_key with functions that require and don't require refresh tokens"""
851+
auth_mgmt_key = "test-auth-mgmt-key"
852+
853+
# Test 1: Direct auth_management_key setting (without refresh token)
854+
client = DescopeClient(
855+
self.dummy_project_id,
856+
self.public_key_dict,
857+
auth_management_key=auth_mgmt_key,
858+
)
859+
860+
with patch("requests.post") as mock_post:
861+
my_mock_response = mock.Mock()
862+
my_mock_response.ok = True
863+
my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"}
864+
mock_post.return_value = my_mock_response
865+
866+
client.otp.sign_up(DeliveryMethod.EMAIL, "[email protected]")
867+
868+
mock_post.assert_called_with(
869+
f"{common.DEFAULT_BASE_URL}{EndpointsV1.sign_up_auth_otp_path}/email",
870+
headers={
871+
**common.default_headers,
872+
"x-descope-project-id": self.dummy_project_id,
873+
"Authorization": f"Bearer {self.dummy_project_id}:{auth_mgmt_key}",
874+
},
875+
json={
876+
"loginId": "[email protected]",
877+
"user": {"email": "[email protected]"},
878+
"email": "[email protected]",
879+
},
880+
params=None,
881+
allow_redirects=False,
882+
verify=True,
883+
timeout=DEFAULT_TIMEOUT_SECONDS,
884+
)
885+
886+
# Test 2: Environment variable auth_management_key setting
887+
env_auth_mgmt_key = "env-auth-mgmt-key"
888+
with patch.dict(
889+
"os.environ", {"DESCOPE_AUTH_MANAGEMENT_KEY": env_auth_mgmt_key}
890+
):
891+
client_env = DescopeClient(self.dummy_project_id, self.public_key_dict)
892+
893+
with patch("requests.post") as mock_post:
894+
my_mock_response = mock.Mock()
895+
my_mock_response.ok = True
896+
my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"}
897+
mock_post.return_value = my_mock_response
898+
899+
client_env.otp.sign_up(DeliveryMethod.EMAIL, "[email protected]")
900+
901+
mock_post.assert_called_with(
902+
f"{common.DEFAULT_BASE_URL}{EndpointsV1.sign_up_auth_otp_path}/email",
903+
headers={
904+
**common.default_headers,
905+
"x-descope-project-id": self.dummy_project_id,
906+
"Authorization": f"Bearer {self.dummy_project_id}:{env_auth_mgmt_key}",
907+
},
908+
json={
909+
"loginId": "[email protected]",
910+
"user": {"email": "[email protected]"},
911+
"email": "[email protected]",
912+
},
913+
allow_redirects=False,
914+
verify=True,
915+
params=None,
916+
timeout=DEFAULT_TIMEOUT_SECONDS,
917+
)
918+
919+
# Test 3: Direct parameter takes priority over environment variable
920+
direct_auth_mgmt_key = "direct-auth-mgmt-key"
921+
with patch.dict(
922+
"os.environ", {"DESCOPE_AUTH_MANAGEMENT_KEY": env_auth_mgmt_key}
923+
):
924+
client_priority = DescopeClient(
925+
self.dummy_project_id,
926+
self.public_key_dict,
927+
auth_management_key=direct_auth_mgmt_key,
928+
)
929+
930+
with patch("requests.post") as mock_post:
931+
my_mock_response = mock.Mock()
932+
my_mock_response.ok = True
933+
my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"}
934+
mock_post.return_value = my_mock_response
935+
936+
client_priority.otp.sign_up(DeliveryMethod.EMAIL, "[email protected]")
937+
938+
mock_post.assert_called_with(
939+
f"{common.DEFAULT_BASE_URL}{EndpointsV1.sign_up_auth_otp_path}/email",
940+
headers={
941+
**common.default_headers,
942+
"x-descope-project-id": self.dummy_project_id,
943+
"Authorization": f"Bearer {self.dummy_project_id}:{direct_auth_mgmt_key}",
944+
},
945+
json={
946+
"loginId": "[email protected]",
947+
"user": {"email": "[email protected]"},
948+
"email": "[email protected]",
949+
},
950+
params=None,
951+
allow_redirects=False,
952+
verify=True,
953+
timeout=DEFAULT_TIMEOUT_SECONDS,
954+
)
955+
956+
def test_auth_management_key_with_refresh_token(self):
957+
auth_mgmt_key = "test-auth-mgmt-key"
958+
client = DescopeClient(
959+
self.dummy_project_id,
960+
self.public_key_dict,
961+
auth_management_key=auth_mgmt_key,
962+
)
963+
964+
# Test with refresh token function
965+
refresh_token = "test_refresh_token"
966+
with patch("requests.post") as mock_post:
967+
my_mock_response = mock.Mock()
968+
my_mock_response.ok = True
969+
my_mock_response.json.return_value = {"maskedEmail": "n***@example.com"}
970+
mock_post.return_value = my_mock_response
971+
972+
client.otp.update_user_email(
973+
974+
)
975+
976+
mock_post.assert_called_with(
977+
f"{common.DEFAULT_BASE_URL}{EndpointsV1.update_user_email_otp_path}",
978+
headers={
979+
**common.default_headers,
980+
"Authorization": f"Bearer {self.dummy_project_id}:{refresh_token}:{auth_mgmt_key}",
981+
"x-descope-project-id": self.dummy_project_id,
982+
},
983+
json={
984+
"loginId": "[email protected]",
985+
"email": "[email protected]",
986+
"addToLoginIDs": False,
987+
"onMergeUseExisting": False,
988+
},
989+
allow_redirects=False,
990+
verify=True,
991+
params=None,
992+
timeout=DEFAULT_TIMEOUT_SECONDS,
993+
)
994+
995+
# Test without auth_management_key for comparison
996+
client_no_auth = DescopeClient(self.dummy_project_id, self.public_key_dict)
997+
with patch("requests.post") as mock_post:
998+
my_mock_response = mock.Mock()
999+
my_mock_response.ok = True
1000+
my_mock_response.json.return_value = {"maskedEmail": "n***@example.com"}
1001+
mock_post.return_value = my_mock_response
1002+
1003+
client_no_auth.otp.update_user_email(
1004+
1005+
)
1006+
1007+
mock_post.assert_called_with(
1008+
f"{common.DEFAULT_BASE_URL}{EndpointsV1.update_user_email_otp_path}",
1009+
headers={
1010+
**common.default_headers,
1011+
"Authorization": f"Bearer {self.dummy_project_id}:{refresh_token}",
1012+
"x-descope-project-id": self.dummy_project_id,
1013+
},
1014+
json={
1015+
"loginId": "[email protected]",
1016+
"email": "[email protected]",
1017+
"addToLoginIDs": False,
1018+
"onMergeUseExisting": False,
1019+
},
1020+
allow_redirects=False,
1021+
verify=True,
1022+
params=None,
1023+
timeout=DEFAULT_TIMEOUT_SECONDS,
1024+
)
1025+
8441026

8451027
if __name__ == "__main__":
8461028
unittest.main()

0 commit comments

Comments
 (0)