Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,23 @@ pip install descope[Flask]
A Descope `Project ID` is required to initialize the SDK. Find it on the
[project page in the Descope Console](https://app.descope.com/settings/project).

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

```python
from descope import DescopeClient

# Initialized after setting the DESCOPE_PROJECT_ID env var
# Initialized after setting the DESCOPE_PROJECT_ID and DESCOPE_AUTH_MANAGEMENT_KEY env vars
descope_client = DescopeClient()

# ** Or directly **
descope_client = DescopeClient(project_id="<Project ID>")
descope_client = DescopeClient(
project_id="<Project ID>"
auth_management_key="<Auth Managemet Key>
)
```

## Authentication Functions
Expand Down Expand Up @@ -1178,7 +1187,7 @@ type doc
permission can_create: owner | parent.owner
permission can_edit: editor | can_create
permission can_view: viewer | can_edit
```
```

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.

Expand Down
6 changes: 6 additions & 0 deletions descope/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def __init__(
management_key: str | None = None,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
jwt_validation_leeway: int = 5,
auth_management_key: str | None = None,
):
self.lock_public_keys = Lock()
# validate project id
Expand All @@ -95,6 +96,9 @@ def __init__(
self.base_url = self.base_url_for_project_id(self.project_id)
self.timeout_seconds = timeout_seconds
self.management_key = management_key or os.getenv("DESCOPE_MANAGEMENT_KEY")
self.auth_management_key = auth_management_key or os.getenv(
"DESCOPE_AUTH_MANAGEMENT_KEY"
)

public_key = public_key or os.getenv("DESCOPE_PUBLIC_KEY")
with self.lock_public_keys:
Expand Down Expand Up @@ -564,6 +568,8 @@ def _get_default_headers(self, pswd: str | None = None):
bearer = self.project_id
if pswd:
bearer = f"{self.project_id}:{pswd}"
if self.auth_management_key:
bearer = f"{bearer}:{self.auth_management_key}"
headers["Authorization"] = f"Bearer {bearer}"
return headers

Expand Down
2 changes: 2 additions & 0 deletions descope/descope_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__(
management_key: str | None = None,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
jwt_validation_leeway: int = 5,
auth_management_key: str | None = None,
):
auth = Auth(
project_id,
Expand All @@ -38,6 +39,7 @@ def __init__(
management_key,
timeout_seconds,
jwt_validation_leeway,
auth_management_key,
)
self._auth = auth
self._mgmt = MGMT(auth)
Expand Down
184 changes: 183 additions & 1 deletion tests/test_descope_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
DescopeClient,
RateLimitException,
)
from descope.common import DEFAULT_TIMEOUT_SECONDS, SESSION_TOKEN_NAME, EndpointsV1
from descope.common import (
DEFAULT_TIMEOUT_SECONDS,
SESSION_TOKEN_NAME,
DeliveryMethod,
EndpointsV1,
)

from . import common

Expand Down Expand Up @@ -841,6 +846,183 @@ def test_select_tenant(self):
timeout=DEFAULT_TIMEOUT_SECONDS,
)

def test_auth_management_key_with_functions(self):
"""Test auth_management_key with functions that require and don't require refresh tokens"""
auth_mgmt_key = "test-auth-mgmt-key"

# Test 1: Direct auth_management_key setting (without refresh token)
client = DescopeClient(
self.dummy_project_id,
self.public_key_dict,
auth_management_key=auth_mgmt_key,
)

with patch("requests.post") as mock_post:
my_mock_response = mock.Mock()
my_mock_response.ok = True
my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"}
mock_post.return_value = my_mock_response

client.otp.sign_up(DeliveryMethod.EMAIL, "test@example.com")

mock_post.assert_called_with(
f"{common.DEFAULT_BASE_URL}{EndpointsV1.sign_up_auth_otp_path}/email",
headers={
**common.default_headers,
"x-descope-project-id": self.dummy_project_id,
"Authorization": f"Bearer {self.dummy_project_id}:{auth_mgmt_key}",
},
json={
"loginId": "test@example.com",
"user": {"email": "test@example.com"},
"email": "test@example.com",
},
params=None,
allow_redirects=False,
verify=True,
timeout=DEFAULT_TIMEOUT_SECONDS,
)

# Test 2: Environment variable auth_management_key setting
env_auth_mgmt_key = "env-auth-mgmt-key"
with patch.dict(
"os.environ", {"DESCOPE_AUTH_MANAGEMENT_KEY": env_auth_mgmt_key}
):
client_env = DescopeClient(self.dummy_project_id, self.public_key_dict)

with patch("requests.post") as mock_post:
my_mock_response = mock.Mock()
my_mock_response.ok = True
my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"}
mock_post.return_value = my_mock_response

client_env.otp.sign_up(DeliveryMethod.EMAIL, "test@example.com")

mock_post.assert_called_with(
f"{common.DEFAULT_BASE_URL}{EndpointsV1.sign_up_auth_otp_path}/email",
headers={
**common.default_headers,
"x-descope-project-id": self.dummy_project_id,
"Authorization": f"Bearer {self.dummy_project_id}:{env_auth_mgmt_key}",
},
json={
"loginId": "test@example.com",
"user": {"email": "test@example.com"},
"email": "test@example.com",
},
allow_redirects=False,
verify=True,
params=None,
timeout=DEFAULT_TIMEOUT_SECONDS,
)

# Test 3: Direct parameter takes priority over environment variable
direct_auth_mgmt_key = "direct-auth-mgmt-key"
with patch.dict(
"os.environ", {"DESCOPE_AUTH_MANAGEMENT_KEY": env_auth_mgmt_key}
):
client_priority = DescopeClient(
self.dummy_project_id,
self.public_key_dict,
auth_management_key=direct_auth_mgmt_key,
)

with patch("requests.post") as mock_post:
my_mock_response = mock.Mock()
my_mock_response.ok = True
my_mock_response.json.return_value = {"maskedEmail": "t***@example.com"}
mock_post.return_value = my_mock_response

client_priority.otp.sign_up(DeliveryMethod.EMAIL, "test@example.com")

mock_post.assert_called_with(
f"{common.DEFAULT_BASE_URL}{EndpointsV1.sign_up_auth_otp_path}/email",
headers={
**common.default_headers,
"x-descope-project-id": self.dummy_project_id,
"Authorization": f"Bearer {self.dummy_project_id}:{direct_auth_mgmt_key}",
},
json={
"loginId": "test@example.com",
"user": {"email": "test@example.com"},
"email": "test@example.com",
},
params=None,
allow_redirects=False,
verify=True,
timeout=DEFAULT_TIMEOUT_SECONDS,
)

def test_auth_management_key_with_refresh_token(self):
auth_mgmt_key = "test-auth-mgmt-key"
client = DescopeClient(
self.dummy_project_id,
self.public_key_dict,
auth_management_key=auth_mgmt_key,
)

# Test with refresh token function
refresh_token = "test_refresh_token"
with patch("requests.post") as mock_post:
my_mock_response = mock.Mock()
my_mock_response.ok = True
my_mock_response.json.return_value = {"maskedEmail": "n***@example.com"}
mock_post.return_value = my_mock_response

client.otp.update_user_email(
"old@example.com", "new@example.com", refresh_token
)

mock_post.assert_called_with(
f"{common.DEFAULT_BASE_URL}{EndpointsV1.update_user_email_otp_path}",
headers={
**common.default_headers,
"Authorization": f"Bearer {self.dummy_project_id}:{refresh_token}:{auth_mgmt_key}",
"x-descope-project-id": self.dummy_project_id,
},
json={
"loginId": "old@example.com",
"email": "new@example.com",
"addToLoginIDs": False,
"onMergeUseExisting": False,
},
allow_redirects=False,
verify=True,
params=None,
timeout=DEFAULT_TIMEOUT_SECONDS,
)

# Test without auth_management_key for comparison
client_no_auth = DescopeClient(self.dummy_project_id, self.public_key_dict)
with patch("requests.post") as mock_post:
my_mock_response = mock.Mock()
my_mock_response.ok = True
my_mock_response.json.return_value = {"maskedEmail": "n***@example.com"}
mock_post.return_value = my_mock_response

client_no_auth.otp.update_user_email(
"old@example.com", "new@example.com", refresh_token
)

mock_post.assert_called_with(
f"{common.DEFAULT_BASE_URL}{EndpointsV1.update_user_email_otp_path}",
headers={
**common.default_headers,
"Authorization": f"Bearer {self.dummy_project_id}:{refresh_token}",
"x-descope-project-id": self.dummy_project_id,
},
json={
"loginId": "old@example.com",
"email": "new@example.com",
"addToLoginIDs": False,
"onMergeUseExisting": False,
},
allow_redirects=False,
verify=True,
params=None,
timeout=DEFAULT_TIMEOUT_SECONDS,
)


if __name__ == "__main__":
unittest.main()
Loading