Skip to content

Commit c751b1e

Browse files
[Feature] Added Outbound App Functions (#621)
* added outbound app functions and tests * fixed tests * added more parameters for outbound apps * added more coverage * modified .gitleaksignore * Add fetching outbound token by using inbound app token + fix linting * fix more lints --------- Co-authored-by: guyp-descope <[email protected]> Co-authored-by: GuyP <[email protected]>
1 parent 6ebd3d6 commit c751b1e

21 files changed

+2216
-46
lines changed

.gitleaksignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,5 @@ e7f5ad4253ad82236a5cff5f8c06878bfb190b00:tests/test_descope_client.py:jwt:185
8585
e7f5ad4253ad82236a5cff5f8c06878bfb190b00:tests/test_descope_client.py:jwt:197
8686
ece761372c78a9ad8a57da5f6d13431d298a99db:tests/test_auth.py:jwt:562
8787
f3ec873c83a7067a1226d8b712b756b1b599fb3b:tests/test_descope_client.py:jwt:519
88+
b6a2e217be5dceb6c85332d2e193619894d3a36e:README.md:generic-api-key:1349
89+
b6a2e217be5dceb6c85332d2e193619894d3a36e:README.md:generic-api-key:1372

README.md

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ These sections show how to use the SDK to perform permission and user management
8282
13. [Manage FGA (Fine-grained Authorization)](#manage-fga-fine-grained-authorization)
8383
14. [Manage Project](#manage-project)
8484
15. [Manage SSO Applications](#manage-sso-applications)
85+
16. [Manage Outbound Applications](#manage-outbound-applications)
8586

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

@@ -1310,6 +1311,169 @@ apps = apps_resp["apps"]
13101311
# Do something
13111312
```
13121313

1314+
### Manage Outbound Applications
1315+
1316+
You can create, update, delete, load outbound applications and fetch tokens for them:
1317+
1318+
```python
1319+
# Create a basic outbound application
1320+
response = descope_client.mgmt.outbound_application.create_application(
1321+
name="my new app",
1322+
description="my desc",
1323+
client_secret="secret123", # Optional
1324+
id="my-custom-id", # Optional
1325+
)
1326+
app_id = response["app"]["id"]
1327+
1328+
# Create a full OAuth outbound application with all parameters
1329+
from descope.management.common import URLParam, AccessType, PromptType
1330+
1331+
# Create URL parameters for authorization
1332+
auth_params = [
1333+
URLParam("response_type", "code"),
1334+
URLParam("client_id", "my-client-id"),
1335+
URLParam("redirect_uri", "https://myapp.com/callback")
1336+
]
1337+
1338+
# Create URL parameters for token endpoint
1339+
token_params = [
1340+
URLParam("grant_type", "authorization_code"),
1341+
URLParam("client_id", "my-client-id")
1342+
]
1343+
1344+
# Create prompt types
1345+
prompts = [PromptType.LOGIN, PromptType.CONSENT]
1346+
1347+
full_app = descope_client.mgmt.outbound_application.create_application(
1348+
name="My OAuth App",
1349+
description="A full OAuth outbound application",
1350+
logo="https://example.com/logo.png",
1351+
id="my-custom-id", # Optional custom ID
1352+
client_secret="my-secret-key",
1353+
client_id="my-client-id",
1354+
discovery_url="https://accounts.google.com/.well-known/openid_configuration",
1355+
authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
1356+
authorization_url_params=auth_params,
1357+
token_url="https://oauth2.googleapis.com/token",
1358+
token_url_params=token_params,
1359+
revocation_url="https://oauth2.googleapis.com/revoke",
1360+
default_scopes=["https://www.googleapis.com/auth/userinfo.profile"],
1361+
default_redirect_url="https://myapp.com/callback",
1362+
callback_domain="myapp.com",
1363+
pkce=True, # Enable PKCE
1364+
access_type=AccessType.OFFLINE, # Request refresh tokens
1365+
prompt=prompts
1366+
)
1367+
1368+
# Update an outbound application with all parameters
1369+
# Update will override all fields as is. Use carefully.
1370+
descope_client.mgmt.outbound_application.update_application(
1371+
id="my-app-id",
1372+
name="my updated app",
1373+
description="updated description",
1374+
logo="https://example.com/logo.png",
1375+
client_secret="new-secret", # Optional
1376+
client_id="new-client-id",
1377+
discovery_url="https://accounts.google.com/.well-known/openid_configuration",
1378+
authorization_url="https://accounts.google.com/o/oauth2/v2/auth",
1379+
authorization_url_params=auth_params,
1380+
token_url="https://oauth2.googleapis.com/token",
1381+
token_url_params=token_params,
1382+
revocation_url="https://oauth2.googleapis.com/revoke",
1383+
default_scopes=["https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"],
1384+
default_redirect_url="https://myapp.com/updated-callback",
1385+
callback_domain="myapp.com",
1386+
pkce=True,
1387+
access_type=AccessType.OFFLINE,
1388+
prompt=[PromptType.LOGIN, PromptType.CONSENT, PromptType.SELECT_ACCOUNT]
1389+
)
1390+
1391+
# Delete an outbound application by id
1392+
# Outbound application deletion cannot be undone. Use carefully.
1393+
descope_client.mgmt.outbound_application.delete_application("my-app-id")
1394+
1395+
# Load an outbound application by id
1396+
app = descope_client.mgmt.outbound_application.load_application("my-app-id")
1397+
1398+
# Load all outbound applications
1399+
apps_resp = descope_client.mgmt.outbound_application.load_all_applications()
1400+
apps = apps_resp["apps"]
1401+
for app in apps:
1402+
# Do something with each app
1403+
1404+
# Fetch user token with specific scopes
1405+
user_token = descope_client.mgmt.outbound_application.fetch_token_by_scopes(
1406+
"my-app-id",
1407+
"user-id",
1408+
["read", "write"],
1409+
{"refreshToken": True}, # Optional
1410+
"tenant-id" # Optional
1411+
)
1412+
1413+
# Fetch latest user token
1414+
latest_user_token = descope_client.mgmt.outbound_application.fetch_token(
1415+
"my-app-id",
1416+
"user-id",
1417+
"tenant-id", # Optional
1418+
{"forceRefresh": True} # Optional
1419+
)
1420+
1421+
# Fetch tenant token with specific scopes
1422+
tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token_by_scopes(
1423+
"my-app-id",
1424+
"tenant-id",
1425+
["read", "write"],
1426+
{"refreshToken": True} # Optional
1427+
)
1428+
1429+
# Fetch latest tenant token
1430+
latest_tenant_token = descope_client.mgmt.outbound_application.fetch_tenant_token(
1431+
"my-app-id",
1432+
"tenant-id",
1433+
{"forceRefresh": True} # Optional
1434+
)
1435+
```
1436+
1437+
Fetch outbound application tokens using an inbound application token that includes the "outbound.token.fetch" scope (no management key required)
1438+
1439+
```python
1440+
# Fetch user token with specific scopes
1441+
user_token = descope_client.mgmt.outbound_application_by_token.fetch_token_by_scopes(
1442+
"inbound-app-token",
1443+
"my-app-id",
1444+
"user-id",
1445+
["read", "write"],
1446+
{"refreshToken": True}, # Optional
1447+
"tenant-id" # Optional
1448+
)
1449+
1450+
# Fetch latest user token
1451+
latest_user_token = descope_client.mgmt.outbound_application_by_token.fetch_token(
1452+
"inbound-app-token",
1453+
"my-app-id",
1454+
"user-id",
1455+
"tenant-id", # Optional
1456+
{"forceRefresh": True} # Optional
1457+
)
1458+
1459+
# Fetch tenant token with specific scopes
1460+
tenant_token = descope_client.mgmt.outbound_application_by_token.fetch_tenant_token_by_scopes(
1461+
"inbound-app-token",
1462+
"my-app-id",
1463+
"tenant-id",
1464+
["read", "write"],
1465+
{"refreshToken": True} # Optional
1466+
)
1467+
1468+
# Fetch latest tenant token
1469+
latest_tenant_token = descope_client.mgmt.outbound_application_by_token.fetch_tenant_token(
1470+
"inbound-app-token",
1471+
"my-app-id",
1472+
"tenant-id",
1473+
{"forceRefresh": True} # Optional
1474+
)
1475+
```
1476+
13131477
### Utils for your end to end (e2e) tests and integration tests
13141478

13151479
To ease your e2e tests, we exposed dedicated management methods,

descope/auth.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def _raise_rate_limit_exception(self, response):
124124
)
125125
except RateLimitException:
126126
raise
127-
except Exception as e:
127+
except Exception:
128128
raise RateLimitException(
129129
status_code=HTTPStatus.TOO_MANY_REQUESTS,
130130
error_type=ERROR_TYPE_API_RATE_LIMIT,

descope/authmethod/enchantedlink.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,13 @@ def update_user_email(
118118
Auth.validate_email(email)
119119

120120
body = EnchantedLink._compose_update_user_email_body(
121-
login_id, email, add_to_login_ids, on_merge_use_existing,
122-
template_options, template_id, provider_id
121+
login_id,
122+
email,
123+
add_to_login_ids,
124+
on_merge_use_existing,
125+
template_options,
126+
template_id,
127+
provider_id,
123128
)
124129
uri = EndpointsV1.update_user_email_enchantedlink_path
125130
response = self._auth.do_post(uri, body, None, refresh_token)

descope/authmethod/magiclink.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,13 @@ def update_user_email(
117117
Auth.validate_email(email)
118118

119119
body = MagicLink._compose_update_user_email_body(
120-
login_id, email, add_to_login_ids, on_merge_use_existing,
121-
template_options, template_id, provider_id
120+
login_id,
121+
email,
122+
add_to_login_ids,
123+
on_merge_use_existing,
124+
template_options,
125+
template_id,
126+
provider_id,
122127
)
123128
uri = EndpointsV1.update_user_email_magiclink_path
124129
response = self._auth.do_post(uri, body, None, refresh_token)
@@ -144,8 +149,13 @@ def update_user_phone(
144149
Auth.validate_phone(method, phone)
145150

146151
body = MagicLink._compose_update_user_phone_body(
147-
login_id, phone, add_to_login_ids, on_merge_use_existing,
148-
template_options, template_id, provider_id
152+
login_id,
153+
phone,
154+
add_to_login_ids,
155+
on_merge_use_existing,
156+
template_options,
157+
template_id,
158+
provider_id,
149159
)
150160
uri = EndpointsV1.update_user_phone_magiclink_path
151161
response = self._auth.do_post(uri, body, None, refresh_token)

descope/authmethod/otp.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,13 @@ def update_user_email(
198198

199199
uri = EndpointsV1.update_user_email_otp_path
200200
body = OTP._compose_update_user_email_body(
201-
login_id, email, add_to_login_ids, on_merge_use_existing,
202-
template_options, template_id, provider_id
201+
login_id,
202+
email,
203+
add_to_login_ids,
204+
on_merge_use_existing,
205+
template_options,
206+
template_id,
207+
provider_id,
203208
)
204209
response = self._auth.do_post(uri, body, None, refresh_token)
205210
return Auth.extract_masked_address(response.json(), DeliveryMethod.EMAIL)
@@ -241,8 +246,13 @@ def update_user_phone(
241246

242247
uri = OTP._compose_update_phone_url(method)
243248
body = OTP._compose_update_user_phone_body(
244-
login_id, phone, add_to_login_ids, on_merge_use_existing,
245-
template_options, template_id, provider_id
249+
login_id,
250+
phone,
251+
add_to_login_ids,
252+
on_merge_use_existing,
253+
template_options,
254+
template_id,
255+
provider_id,
246256
)
247257
response = self._auth.do_post(uri, body, None, refresh_token)
248258
return Auth.extract_masked_address(response.json(), method)

descope/descope_client.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,6 @@ def __init__(
5555

5656
@property
5757
def mgmt(self):
58-
if not self._auth.management_key:
59-
raise AuthException(
60-
400, ERROR_TYPE_INVALID_ARGUMENT, "management_key cannot be empty"
61-
)
6258
return self._mgmt
6359

6460
@property

descope/flask/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import uuid
55
from functools import wraps
66

7-
from flask import Response, redirect, request, g
7+
from flask import Response, g, redirect, request
88

99
from .. import (
1010
COOKIE_DATA_NAME,

descope/management/common.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,34 @@
1+
from enum import Enum
12
from typing import List, Optional
23

34

5+
class AccessType(Enum):
6+
OFFLINE = "offline"
7+
ONLINE = "online"
8+
9+
10+
class PromptType(Enum):
11+
NONE = "none"
12+
LOGIN = "login"
13+
CONSENT = "consent"
14+
SELECT_ACCOUNT = "select_account"
15+
16+
17+
class URLParam:
18+
def __init__(self, name: str, value: str):
19+
self.name = name
20+
self.value = value
21+
22+
def to_dict(self) -> dict:
23+
return {"name": self.name, "value": self.value}
24+
25+
26+
def url_params_to_dict(url_params: Optional[List[URLParam]] = None) -> list:
27+
if url_params is None:
28+
return []
29+
return [param.to_dict() for param in url_params]
30+
31+
432
class MgmtV1:
533
# tenant
634
tenant_create_path = "/v1/mgmt/tenant/create"
@@ -19,6 +47,21 @@ class MgmtV1:
1947
sso_application_load_path = "/v1/mgmt/sso/idp/app/load"
2048
sso_application_load_all_path = "/v1/mgmt/sso/idp/apps/load"
2149

50+
# outbound application
51+
outbound_application_create_path = "/v1/mgmt/outbound/app/create"
52+
outbound_application_update_path = "/v1/mgmt/outbound/app/update"
53+
outbound_application_delete_path = "/v1/mgmt/outbound/app/delete"
54+
outbound_application_load_path = "/v1/mgmt/outbound/app"
55+
outbound_application_load_all_path = "/v1/mgmt/outbound/apps"
56+
outbound_application_fetch_token_by_scopes_path = "/v1/mgmt/outbound/app/user/token"
57+
outbound_application_fetch_token_path = "/v1/mgmt/outbound/app/user/token/latest"
58+
outbound_application_fetch_tenant_token_by_scopes_path = (
59+
"/v1/mgmt/outbound/app/tenant/token"
60+
)
61+
outbound_application_fetch_tenant_token_path = (
62+
"/v1/mgmt/outbound/app/tenant/token/latest"
63+
)
64+
2265
# user
2366
user_create_path = "/v1/mgmt/user/create"
2467
test_user_create_path = "/v1/mgmt/user/create/test"
@@ -365,7 +408,8 @@ def sort_to_dict(sort: List[Sort]) -> list:
365408
)
366409
return sort_list
367410

411+
368412
def map_to_values_object(input_map: dict):
369413
if not input_map:
370414
return {}
371-
return {k: {"values": v} for k, v in input_map.items()}
415+
return {k: {"values": v} for k, v in input_map.items()}

descope/management/fga.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from datetime import datetime, timezone
2-
from typing import Any, List, Optional
1+
from typing import List
32

43
from descope._auth_base import AuthBase
54
from descope.management.common import MgmtV1

0 commit comments

Comments
 (0)