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
2 changes: 2 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,5 @@ e7f5ad4253ad82236a5cff5f8c06878bfb190b00:tests/test_descope_client.py:jwt:185
e7f5ad4253ad82236a5cff5f8c06878bfb190b00:tests/test_descope_client.py:jwt:197
ece761372c78a9ad8a57da5f6d13431d298a99db:tests/test_auth.py:jwt:562
f3ec873c83a7067a1226d8b712b756b1b599fb3b:tests/test_descope_client.py:jwt:519
b6a2e217be5dceb6c85332d2e193619894d3a36e:README.md:generic-api-key:1349
b6a2e217be5dceb6c85332d2e193619894d3a36e:README.md:generic-api-key:1372
164 changes: 164 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ These sections show how to use the SDK to perform permission and user management
13. [Manage FGA (Fine-grained Authorization)](#manage-fga-fine-grained-authorization)
14. [Manage Project](#manage-project)
15. [Manage SSO Applications](#manage-sso-applications)
16. [Manage Outbound Applications](#manage-outbound-applications)

If you wish to run any of our code samples and play with them, check out our [Code Examples](#code-examples) section.

Expand Down Expand Up @@ -1310,6 +1311,169 @@ apps = apps_resp["apps"]
# Do something
```

### Manage Outbound Applications

You can create, update, delete, load outbound applications and fetch tokens for them:

```python
# Create a basic outbound application
response = descope_client.mgmt.outbound_application.create_application(
name="my new app",
description="my desc",
client_secret="secret123", # Optional
id="my-custom-id", # Optional
)
app_id = response["app"]["id"]

# Create a full OAuth outbound application with all parameters
from descope.management.common import URLParam, AccessType, PromptType

# Create URL parameters for authorization
auth_params = [
URLParam("response_type", "code"),
URLParam("client_id", "my-client-id"),
URLParam("redirect_uri", "https://myapp.com/callback")
]

# Create URL parameters for token endpoint
token_params = [
URLParam("grant_type", "authorization_code"),
URLParam("client_id", "my-client-id")
]

# Create prompt types
prompts = [PromptType.LOGIN, PromptType.CONSENT]

full_app = descope_client.mgmt.outbound_application.create_application(
name="My OAuth App",
description="A full OAuth outbound application",
logo="https://example.com/logo.png",
id="my-custom-id", # Optional custom ID
client_secret="my-secret-key",
client_id="my-client-id",
discovery_url="https://accounts.google.com/.well-known/openid_configuration",
authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
authorization_url_params=auth_params,
token_url="https://oauth2.googleapis.com/token",
token_url_params=token_params,
revocation_url="https://oauth2.googleapis.com/revoke",
default_scopes=["https://www.googleapis.com/auth/userinfo.profile"],
default_redirect_url="https://myapp.com/callback",
callback_domain="myapp.com",
pkce=True, # Enable PKCE
access_type=AccessType.OFFLINE, # Request refresh tokens
prompt=prompts
)

# Update an outbound application with all parameters
# Update will override all fields as is. Use carefully.
descope_client.mgmt.outbound_application.update_application(
id="my-app-id",
name="my updated app",
description="updated description",
logo="https://example.com/logo.png",
client_secret="new-secret", # Optional
client_id="new-client-id",
discovery_url="https://accounts.google.com/.well-known/openid_configuration",
authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
authorization_url_params=auth_params,
token_url="https://oauth2.googleapis.com/token",
token_url_params=token_params,
revocation_url="https://oauth2.googleapis.com/revoke",
default_scopes=["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"],
default_redirect_url="https://myapp.com/updated-callback",
callback_domain="myapp.com",
pkce=True,
access_type=AccessType.OFFLINE,
prompt=[PromptType.LOGIN, PromptType.CONSENT, PromptType.SELECT_ACCOUNT]
)

# Delete an outbound application by id
# Outbound application deletion cannot be undone. Use carefully.
descope_client.mgmt.outbound_application.delete_application("my-app-id")

# Load an outbound application by id
app = descope_client.mgmt.outbound_application.load_application("my-app-id")

# Load all outbound applications
apps_resp = descope_client.mgmt.outbound_application.load_all_applications()
apps = apps_resp["apps"]
for app in apps:
# Do something with each app

# Fetch user token with specific scopes
user_token = descope_client.mgmt.outbound_application.fetch_token_by_scopes(
"my-app-id",
"user-id",
["read", "write"],
{"refreshToken": True}, # Optional
"tenant-id" # Optional
)

# Fetch latest user token
latest_user_token = descope_client.mgmt.outbound_application.fetch_token(
"my-app-id",
"user-id",
"tenant-id", # Optional
{"forceRefresh": True} # Optional
)

# Fetch tenant token with specific scopes
tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token_by_scopes(
"my-app-id",
"tenant-id",
["read", "write"],
{"refreshToken": True} # Optional
)

# Fetch latest tenant token
latest_tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token(
"my-app-id",
"tenant-id",
{"forceRefresh": True} # Optional
)
```

Fetch outbound application tokens using an inbound application token that includes the "outbound.token.fetch" scope (no management key required)

```python
# Fetch user token with specific scopes
user_token = descope_client.mgmt.outbound_application_by_token.fetch_token_by_scopes(
"inbound-app-token",
"my-app-id",
"user-id",
["read", "write"],
{"refreshToken": True}, # Optional
"tenant-id" # Optional
)

# Fetch latest user token
latest_user_token = descope_client.mgmt.outbound_application_by_token.fetch_token(
"inbound-app-token",
"my-app-id",
"user-id",
"tenant-id", # Optional
{"forceRefresh": True} # Optional
)

# Fetch tenant token with specific scopes
tenant_token = descope_client.mgmt.outbound_application_by_token.fetch_tenant_token_by_scopes(
"inbound-app-token",
"my-app-id",
"tenant-id",
["read", "write"],
{"refreshToken": True} # Optional
)

# Fetch latest tenant token
latest_tenant_token = descope_client.mgmt.outbound_application_by_token.fetch_tenant_token(
"inbound-app-token",
"my-app-id",
"tenant-id",
{"forceRefresh": True} # Optional
)
```

### Utils for your end to end (e2e) tests and integration tests

To ease your e2e tests, we exposed dedicated management methods,
Expand Down
2 changes: 1 addition & 1 deletion descope/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def _raise_rate_limit_exception(self, response):
)
except RateLimitException:
raise
except Exception as e:
except Exception:
raise RateLimitException(
status_code=HTTPStatus.TOO_MANY_REQUESTS,
error_type=ERROR_TYPE_API_RATE_LIMIT,
Expand Down
9 changes: 7 additions & 2 deletions descope/authmethod/enchantedlink.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,13 @@ def update_user_email(
Auth.validate_email(email)

body = EnchantedLink._compose_update_user_email_body(
login_id, email, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
email,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
uri = EndpointsV1.update_user_email_enchantedlink_path
response = self._auth.do_post(uri, body, None, refresh_token)
Expand Down
18 changes: 14 additions & 4 deletions descope/authmethod/magiclink.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,13 @@ def update_user_email(
Auth.validate_email(email)

body = MagicLink._compose_update_user_email_body(
login_id, email, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
email,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
uri = EndpointsV1.update_user_email_magiclink_path
response = self._auth.do_post(uri, body, None, refresh_token)
Expand All @@ -144,8 +149,13 @@ def update_user_phone(
Auth.validate_phone(method, phone)

body = MagicLink._compose_update_user_phone_body(
login_id, phone, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
phone,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
uri = EndpointsV1.update_user_phone_magiclink_path
response = self._auth.do_post(uri, body, None, refresh_token)
Expand Down
18 changes: 14 additions & 4 deletions descope/authmethod/otp.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,13 @@ def update_user_email(

uri = EndpointsV1.update_user_email_otp_path
body = OTP._compose_update_user_email_body(
login_id, email, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
email,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
response = self._auth.do_post(uri, body, None, refresh_token)
return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL)
Expand Down Expand Up @@ -241,8 +246,13 @@ def update_user_phone(

uri = OTP._compose_update_phone_url(method)
body = OTP._compose_update_user_phone_body(
login_id, phone, add_to_login_ids, on_merge_use_existing,
template_options, template_id, provider_id
login_id,
phone,
add_to_login_ids,
on_merge_use_existing,
template_options,
template_id,
provider_id,
)
response = self._auth.do_post(uri, body, None, refresh_token)
return Auth.extract_masked_address(response.json(), method)
Expand Down
4 changes: 0 additions & 4 deletions descope/descope_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@ def __init__(

@property
def mgmt(self):
if not self._auth.management_key:
raise AuthException(
400, ERROR_TYPE_INVALID_ARGUMENT, "management_key cannot be empty"
)
return self._mgmt

@property
Expand Down
2 changes: 1 addition & 1 deletion descope/flask/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import uuid
from functools import wraps

from flask import Response, redirect, request, g
from flask import Response, g, redirect, request

from .. import (
COOKIE_DATA_NAME,
Expand Down
46 changes: 45 additions & 1 deletion descope/management/common.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
from enum import Enum
from typing import List, Optional


class AccessType(Enum):
OFFLINE = "offline"
ONLINE = "online"


class PromptType(Enum):
NONE = "none"
LOGIN = "login"
CONSENT = "consent"
SELECT_ACCOUNT = "select_account"


class URLParam:
def __init__(self, name: str, value: str):
self.name = name
self.value = value

def to_dict(self) -> dict:
return {"name": self.name, "value": self.value}


def url_params_to_dict(url_params: Optional[List[URLParam]] = None) -> list:
if url_params is None:
return []

Check warning on line 28 in descope/management/common.py

View workflow job for this annotation

GitHub Actions / Coverage

This line has no coverage
return [param.to_dict() for param in url_params]


class MgmtV1:
# tenant
tenant_create_path = "/v1/mgmt/tenant/create"
Expand All @@ -19,6 +47,21 @@
sso_application_load_path = "/v1/mgmt/sso/idp/app/load"
sso_application_load_all_path = "/v1/mgmt/sso/idp/apps/load"

# outbound application
outbound_application_create_path = "/v1/mgmt/outbound/app/create"
outbound_application_update_path = "/v1/mgmt/outbound/app/update"
outbound_application_delete_path = "/v1/mgmt/outbound/app/delete"
outbound_application_load_path = "/v1/mgmt/outbound/app"
outbound_application_load_all_path = "/v1/mgmt/outbound/apps"
outbound_application_fetch_token_by_scopes_path = "/v1/mgmt/outbound/app/user/token"
outbound_application_fetch_token_path = "/v1/mgmt/outbound/app/user/token/latest"
outbound_application_fetch_tenant_token_by_scopes_path = (
"/v1/mgmt/outbound/app/tenant/token"
)
outbound_application_fetch_tenant_token_path = (
"/v1/mgmt/outbound/app/tenant/token/latest"
)

# user
user_create_path = "/v1/mgmt/user/create"
test_user_create_path = "/v1/mgmt/user/create/test"
Expand Down Expand Up @@ -365,7 +408,8 @@
)
return sort_list


def map_to_values_object(input_map: dict):
if not input_map:
return {}
return {k: {"values": v} for k, v in input_map.items()}
return {k: {"values": v} for k, v in input_map.items()}
3 changes: 1 addition & 2 deletions descope/management/fga.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from datetime import datetime, timezone
from typing import Any, List, Optional
from typing import List

from descope._auth_base import AuthBase
from descope.management.common import MgmtV1
Expand Down
6 changes: 2 additions & 4 deletions descope/management/jwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def impersonate(
pswd=self._auth.management_key,
)
return response.json().get("jwt", "")

def stop_impersonation(
self,
jwt: str,
Expand All @@ -109,9 +109,7 @@ def stop_impersonation(
AuthException: raised if update failed
"""
if not jwt or jwt == "":
raise AuthException(
400, ERROR_TYPE_INVALID_ARGUMENT, "jwt cannot be empty"
)
raise AuthException(400, ERROR_TYPE_INVALID_ARGUMENT, "jwt cannot be empty")

response = self._auth.do_post(
MgmtV1.stop_impersonation_path,
Expand Down
Loading
Loading