diff --git a/README.md b/README.md
index 6ffc057..54d9b89 100644
--- a/README.md
+++ b/README.md
@@ -242,6 +242,34 @@ $response = $descopeSDK->auth->sso->exchangeToken("code");
print_r($response);
```
+### OTP (One-Time Password)
+
+The delivery method is one of `"email"`, `"sms"`, `"whatsapp"` or `"voice"`.
+
+```php
+// Send a code to a new or existing user
+$descopeSDK->otp->signUp("email", "loginId", ["email" => "user@example.com"]);
+$descopeSDK->otp->signIn("email", "loginId");
+$descopeSDK->otp->signUpOrIn("email", "loginId");
+
+// Verify the received code and get a session
+$response = $descopeSDK->otp->verifyCode("email", "loginId", "123456");
+print_r($response);
+```
+
+### Magic Link
+
+```php
+// Send a magic link containing the given redirect URI
+$descopeSDK->magicLink->signUp("email", "loginId", "https://example.com/verify", ["email" => "user@example.com"]);
+$descopeSDK->magicLink->signIn("email", "loginId", "https://example.com/verify");
+$descopeSDK->magicLink->signUpOrIn("email", "loginId", "https://example.com/verify");
+
+// Verify the token extracted from the magic link and get a session
+$response = $descopeSDK->magicLink->verify("token");
+print_r($response);
+```
+
### Session Management
1. `DescopeSDK->verify($sessionToken)` - will validate the session token and return either **TRUE** or **FALSE**, depending on if the JWT is valid and expired.
@@ -254,6 +282,9 @@ print_r($response);
6. `DescopeSDK->getClaims($sessionToken)` - will validate the JWT signature and return all of the verified claims in an array format.
7. `DescopeSDK->getUserDetails($refreshToken)` - will return all of the user information (email, phone, verification status, etc.) using a provided refresh token.
+8. `DescopeSDK->selectTenant($tenantId, $refreshToken)` - will return a new set of tokens scoped to the selected tenant.
+9. `DescopeSDK->exchangeAccessKey($accessKey, $loginOptions)` - will exchange an access key for a session JWT.
+10. `DescopeSDK->history($refreshToken)` - will return the current user's authentication history.
### User Management Functions
@@ -461,6 +492,133 @@ $descopeSDK->management->user->setTemporaryPassword("testuser1", new UserPasswor
$descopeSDK->management->user->setActivePassword("testuser1", new UserPassword(cleartext: "activePassword123"));
```
+### Tenant Management Functions
+
+Manage tenants for multi-tenant applications.
+
+```php
+// Create a tenant (id is optional; one is generated if omitted)
+$response = $descopeSDK->management->tenant->create("My Tenant", null, ["example.com"], ["plan" => "pro"]);
+$tenantId = $response["id"];
+
+// Update a tenant (overwrites all fields)
+$descopeSDK->management->tenant->update($tenantId, "My Renamed Tenant", ["example.com"]);
+
+// Load a single tenant / all tenants
+$tenant = $descopeSDK->management->tenant->load($tenantId);
+$all = $descopeSDK->management->tenant->loadAll();
+
+// Search tenants
+$found = $descopeSDK->management->tenant->searchAll([], ["My Renamed Tenant"]);
+
+// Delete a tenant (cascade removes its users/keys)
+$descopeSDK->management->tenant->delete($tenantId, false);
+```
+
+### Role Management Functions
+
+```php
+$descopeSDK->management->role->create("My Role", "role description", ["Read", "Write"]);
+$descopeSDK->management->role->update("My Role", "My Renamed Role", "updated", ["Read"]);
+$roles = $descopeSDK->management->role->loadAll();
+$matches = $descopeSDK->management->role->search([], ["My Renamed Role"]);
+$descopeSDK->management->role->delete("My Renamed Role");
+```
+
+### Permission Management Functions
+
+```php
+$descopeSDK->management->permission->create("Read", "can read");
+$descopeSDK->management->permission->update("Read", "ReadOnly", "can read only");
+$permissions = $descopeSDK->management->permission->loadAll();
+$descopeSDK->management->permission->delete("ReadOnly");
+```
+
+### Access Key Management Functions
+
+```php
+// Create an access key; the cleartext is only returned once, on creation
+$response = $descopeSDK->management->accessKey->create("My Key", 0, ["My Role"]);
+$cleartext = $response["cleartext"];
+$keyId = $response["key"]["id"];
+
+$descopeSDK->management->accessKey->load($keyId);
+$descopeSDK->management->accessKey->searchAll();
+$descopeSDK->management->accessKey->update($keyId, "My Renamed Key");
+$descopeSDK->management->accessKey->deactivate($keyId);
+$descopeSDK->management->accessKey->activate($keyId);
+$descopeSDK->management->accessKey->delete($keyId);
+```
+
+### SSO Application Management Functions
+
+Manage SSO (IdP) applications your project exposes to relying parties.
+
+```php
+// OIDC application
+$response = $descopeSDK->management->ssoApplication->createOidcApplication("My OIDC App", "https://login.example.com");
+$appId = $response["id"];
+$descopeSDK->management->ssoApplication->updateOidcApplication($appId, "My OIDC App", "https://login.example.com");
+
+// SAML application
+$descopeSDK->management->ssoApplication->createSamlApplication("My SAML App", "https://login.example.com");
+
+$descopeSDK->management->ssoApplication->load($appId);
+$descopeSDK->management->ssoApplication->loadAll();
+$descopeSDK->management->ssoApplication->delete($appId);
+```
+
+### SSO Settings (Tenant SSO Configuration)
+
+Configure the SSO provider used by a tenant. `management->sso` is the tenant SSO configuration component (distinct from the auth-flow `$descopeSDK->sso`).
+
+```php
+$settings = $descopeSDK->management->sso->loadSettings("tenantId1");
+
+// Configure OIDC for a tenant
+$descopeSDK->management->sso->configureOIDCSettings("tenantId1", [
+ "name" => "MyOIDC",
+ "clientId" => "clientId",
+ "clientSecret" => "clientSecret",
+ "redirectUrl" => "https://example.com/callback",
+ "authUrl" => "https://idp.example.com/authorize",
+ "tokenUrl" => "https://idp.example.com/token",
+ "userDataUrl" => "https://idp.example.com/userinfo",
+ "scope" => ["openid", "email"],
+], ["example.com"]);
+
+// Configure SAML for a tenant
+$descopeSDK->management->sso->configureSAMLSettings("tenantId1", [
+ "idpUrl" => "https://idp.example.com/sso",
+ "entityId" => "entityId",
+ "idpCert" => "-----BEGIN CERTIFICATE----- ...",
+], "https://example.com/callback", ["example.com"]);
+
+$descopeSDK->management->sso->deleteSettings("tenantId1");
+```
+
+### JWT Management Functions
+
+```php
+// Update a JWT with custom claims
+$newJwt = $descopeSDK->management->jwt->updateJWT($jwt, ["myClaim" => "value"]);
+
+// Impersonate a user (requires the impersonator to have permission)
+$impersonatedJwt = $descopeSDK->management->jwt->impersonate("impersonatorId", "targetLoginId", true);
+```
+
+### Flow & Theme Management Functions
+
+```php
+$flows = $descopeSDK->management->flow->listFlows();
+$exported = $descopeSDK->management->flow->exportFlow("sign-up-or-in");
+$descopeSDK->management->flow->importFlow("sign-up-or-in", $exported["flow"], $exported["screens"] ?? []);
+$descopeSDK->management->flow->delete(["old-flow-id"]);
+
+$theme = $descopeSDK->management->flow->exportTheme();
+$descopeSDK->management->flow->importTheme($theme["theme"]);
+```
+
## Password Management
The SDK provides several classes for handling different types of passwords and password hashes. Here's how to use them:
diff --git a/phpunit.xml b/phpunit.xml
index 0e890f1..ec98877 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -9,6 +9,8 @@
src/tests/APIRetryTest.php
src/tests/StaticStateIsolationTest.php
src/tests/EndpointsTest.php
+ src/tests/Management/ManagementParityTest.php
+ src/tests/Auth/AuthParityTest.php
\ No newline at end of file
diff --git a/src/SDK/Auth/MagicLink.php b/src/SDK/Auth/MagicLink.php
new file mode 100644
index 0000000..348768d
--- /dev/null
+++ b/src/SDK/Auth/MagicLink.php
@@ -0,0 +1,166 @@
+api = $api;
+ }
+
+ /**
+ * Sign up a new user and send them a magic link.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user being signed up.
+ * @param string $uri The redirect URI embedded in the magic link.
+ * @param array|null $user Optional user details (e.g. email, phone, name).
+ * @param array|null $signUpOptions Optional sign-up options (customClaims, templateOptions).
+ * @return string The masked address the magic link was sent to.
+ * @throws AuthException
+ */
+ public function signUp(string $deliveryMethod, string $loginId, string $uri, ?array $user = null, ?array $signUpOptions = null): string
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ $body = ['loginId' => $loginId, 'uri' => $uri];
+ if ($user !== null) {
+ $body['user'] = $user;
+ }
+ if ($signUpOptions !== null) {
+ $body['loginOptions'] = $signUpOptions;
+ }
+
+ $endpoint = EndpointsV1::$SIGN_UP_AUTH_MAGICLINK_PATH . '/' . $method;
+ $response = $this->api->doPost($endpoint, $body, false);
+
+ return $response[$method] ?? '';
+ }
+
+ /**
+ * Sign in an existing user and send them a magic link.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user signing in.
+ * @param string $uri The redirect URI embedded in the magic link.
+ * @param array|null $loginOptions Optional login options.
+ * @param string|null $refreshToken Optional refresh token for step-up/MFA.
+ * @return string The masked address the magic link was sent to.
+ * @throws AuthException
+ */
+ public function signIn(string $deliveryMethod, string $loginId, string $uri, ?array $loginOptions = null, ?string $refreshToken = null): string
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ $body = ['loginId' => $loginId, 'uri' => $uri];
+ if ($loginOptions !== null) {
+ $body['loginOptions'] = $loginOptions;
+ }
+
+ $endpoint = EndpointsV1::$SIGN_IN_AUTH_MAGICLINK_PATH . '/' . $method;
+ $response = $this->api->doPost($endpoint, $body, false, $refreshToken);
+
+ return $response[$method] ?? '';
+ }
+
+ /**
+ * Sign up or sign in a user (whichever applies) and send them a magic link.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user.
+ * @param string $uri The redirect URI embedded in the magic link.
+ * @param array|null $loginOptions Optional login options.
+ * @return string The masked address the magic link was sent to.
+ * @throws AuthException
+ */
+ public function signUpOrIn(string $deliveryMethod, string $loginId, string $uri, ?array $loginOptions = null): string
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ $body = ['loginId' => $loginId, 'uri' => $uri];
+ if ($loginOptions !== null) {
+ $body['loginOptions'] = $loginOptions;
+ }
+
+ $endpoint = EndpointsV1::$SIGN_UP_OR_IN_AUTH_MAGICLINK_PATH . '/' . $method;
+ $response = $this->api->doPost($endpoint, $body, false);
+
+ return $response[$method] ?? '';
+ }
+
+ /**
+ * Verify a magic-link token and complete the authentication.
+ *
+ * @param string $token The token extracted from the magic link.
+ * @return array JWT response array.
+ * @throws AuthException
+ */
+ public function verify(string $token): array
+ {
+ if (empty($token)) {
+ throw new AuthException(400, 'invalid argument', 'token cannot be empty');
+ }
+
+ $response = $this->api->doPost(EndpointsV1::$VERIFY_MAGICLINK_AUTH_PATH, ['token' => $token], false);
+
+ return $this->api->generateJwtResponse($response, $response['refreshJwt'] ?? null, null);
+ }
+
+ /**
+ * Validates the delivery method and returns its normalized string form.
+ *
+ * @param string $deliveryMethod The requested delivery method.
+ * @return string The normalized (lowercase) delivery method.
+ * @throws AuthException
+ */
+ private function validateMethod(string $deliveryMethod): string
+ {
+ $method = strtolower($deliveryMethod);
+ if (!in_array($method, self::METHODS, true)) {
+ throw new AuthException(400, 'invalid argument', "Unknown delivery method: $deliveryMethod");
+ }
+ return $method;
+ }
+
+ /**
+ * Validates the login ID.
+ *
+ * @param string $loginId The login ID.
+ * @throws AuthException
+ */
+ private function validateLoginId(string $loginId): void
+ {
+ if (empty($loginId)) {
+ throw new AuthException(400, 'invalid argument', 'login_id cannot be empty');
+ }
+ }
+}
diff --git a/src/SDK/Auth/OTP.php b/src/SDK/Auth/OTP.php
new file mode 100644
index 0000000..8db3a22
--- /dev/null
+++ b/src/SDK/Auth/OTP.php
@@ -0,0 +1,168 @@
+api = $api;
+ }
+
+ /**
+ * Sign up a new user and send them an OTP code.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user being signed up.
+ * @param array|null $user Optional user details (e.g. email, phone, name).
+ * @param array|null $signUpOptions Optional sign-up options (customClaims, templateOptions).
+ * @return string The masked address the OTP was sent to.
+ * @throws AuthException
+ */
+ public function signUp(string $deliveryMethod, string $loginId, ?array $user = null, ?array $signUpOptions = null): string
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ $body = ['loginId' => $loginId];
+ if ($user !== null) {
+ $body['user'] = $user;
+ }
+ if ($signUpOptions !== null) {
+ $body['loginOptions'] = $signUpOptions;
+ }
+
+ $uri = EndpointsV1::$SIGN_UP_AUTH_OTP_PATH . '/' . $method;
+ $response = $this->api->doPost($uri, $body, false);
+
+ return $response[$method] ?? '';
+ }
+
+ /**
+ * Sign in an existing user and send them an OTP code.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user signing in.
+ * @param array|null $loginOptions Optional login options.
+ * @param string|null $refreshToken Optional refresh token for step-up/MFA.
+ * @return string The masked address the OTP was sent to.
+ * @throws AuthException
+ */
+ public function signIn(string $deliveryMethod, string $loginId, ?array $loginOptions = null, ?string $refreshToken = null): string
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ $body = ['loginId' => $loginId];
+ if ($loginOptions !== null) {
+ $body['loginOptions'] = $loginOptions;
+ }
+
+ $uri = EndpointsV1::$SIGN_IN_AUTH_OTP_PATH . '/' . $method;
+ $response = $this->api->doPost($uri, $body, false, $refreshToken);
+
+ return $response[$method] ?? '';
+ }
+
+ /**
+ * Sign up or sign in a user (whichever applies) and send them an OTP code.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user.
+ * @param array|null $loginOptions Optional login options.
+ * @return string The masked address the OTP was sent to.
+ * @throws AuthException
+ */
+ public function signUpOrIn(string $deliveryMethod, string $loginId, ?array $loginOptions = null): string
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ $body = ['loginId' => $loginId];
+ if ($loginOptions !== null) {
+ $body['loginOptions'] = $loginOptions;
+ }
+
+ $uri = EndpointsV1::$SIGN_UP_OR_IN_AUTH_OTP_PATH . '/' . $method;
+ $response = $this->api->doPost($uri, $body, false);
+
+ return $response[$method] ?? '';
+ }
+
+ /**
+ * Verify an OTP code and complete the authentication.
+ *
+ * @param string $deliveryMethod One of "email", "sms", "whatsapp", "voice".
+ * @param string $loginId The login ID of the user.
+ * @param string $code The OTP code the user received.
+ * @return array JWT response array.
+ * @throws AuthException
+ */
+ public function verifyCode(string $deliveryMethod, string $loginId, string $code): array
+ {
+ $method = $this->validateMethod($deliveryMethod);
+ $this->validateLoginId($loginId);
+
+ if (empty($code)) {
+ throw new AuthException(400, 'invalid argument', 'code cannot be empty');
+ }
+
+ $uri = EndpointsV1::$VERIFY_CODE_AUTH_PATH . '/' . $method;
+ $response = $this->api->doPost($uri, ['loginId' => $loginId, 'code' => $code], false);
+
+ return $this->api->generateJwtResponse($response, $response['refreshJwt'] ?? null, null);
+ }
+
+ /**
+ * Validates the delivery method and returns its normalized string form.
+ *
+ * @param string $deliveryMethod The requested delivery method.
+ * @return string The normalized (lowercase) delivery method.
+ * @throws AuthException
+ */
+ private function validateMethod(string $deliveryMethod): string
+ {
+ $method = strtolower($deliveryMethod);
+ if (!in_array($method, self::METHODS, true)) {
+ throw new AuthException(400, 'invalid argument', "Unknown delivery method: $deliveryMethod");
+ }
+ return $method;
+ }
+
+ /**
+ * Validates the login ID.
+ *
+ * @param string $loginId The login ID.
+ * @throws AuthException
+ */
+ private function validateLoginId(string $loginId): void
+ {
+ if (empty($loginId)) {
+ throw new AuthException(400, 'invalid argument', 'login_id cannot be empty');
+ }
+ }
+}
diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php
index 85a7836..63c9ff5 100644
--- a/src/SDK/DescopeSDK.php
+++ b/src/SDK/DescopeSDK.php
@@ -9,6 +9,8 @@
use Descope\SDK\Auth\Password;
use Descope\SDK\Auth\SSO;
use Descope\SDK\Auth\OAuth;
+use Descope\SDK\Auth\OTP;
+use Descope\SDK\Auth\MagicLink;
use Descope\SDK\Management\Management;
use Descope\SDK\Auth\Management\User;
use Descope\SDK\Auth\Management\Audit;
@@ -26,6 +28,8 @@ class DescopeSDK
public Password $password;
public SSO $sso;
public OAuth $oauth;
+ public OTP $otp;
+ public MagicLink $magicLink;
public Management $management;
public API $api;
private Verifier $verifier;
@@ -70,6 +74,8 @@ public function __construct(array $config)
$this->password = new Password($this->api);
$this->sso = new SSO($this->api);
$this->oauth = new OAuth($this->api);
+ $this->otp = new OTP($this->api);
+ $this->magicLink = new MagicLink($this->api);
}
/**
@@ -226,6 +232,86 @@ public function logoutAll(?string $refreshToken = null): void
);
}
+ /**
+ * Select a tenant for the current session, returning a new set of tokens
+ * scoped to that tenant.
+ *
+ * @param string $tenantId The tenant to select.
+ * @param string|null $refreshToken The refresh token of the current session.
+ * @return array The new session information.
+ * @throws AuthException|RateLimitException
+ */
+ public function selectTenant(string $tenantId, ?string $refreshToken = null): array
+ {
+ if (empty($tenantId)) {
+ throw new ValidationException('Tenant ID cannot be null or empty.');
+ }
+
+ $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null;
+
+ if (empty($refreshToken)) {
+ throw ValidationException::forMissingRefreshToken();
+ }
+
+ $response = $this->api->doPost(
+ EndpointsV1::$SELECT_TENANT_PATH,
+ ['tenant' => $tenantId],
+ false,
+ $refreshToken
+ );
+
+ return $this->api->generateJwtResponse($response, $response['refreshJwt'] ?? null, null);
+ }
+
+ /**
+ * Exchange an access key for a session JWT.
+ *
+ * @param string $accessKey The access key to exchange.
+ * @param array|null $loginOptions Optional login options (e.g. customClaims).
+ * @return array The API response containing the session JWT.
+ * @throws AuthException|RateLimitException
+ */
+ public function exchangeAccessKey(string $accessKey, ?array $loginOptions = null): array
+ {
+ if (empty($accessKey)) {
+ throw new ValidationException('Access key cannot be null or empty.');
+ }
+
+ $body = [];
+ if ($loginOptions !== null) {
+ $body['loginOptions'] = $loginOptions;
+ }
+
+ return $this->api->doPost(
+ EndpointsV1::$EXCHANGE_AUTH_ACCESS_KEY_PATH,
+ $body,
+ false,
+ $accessKey
+ );
+ }
+
+ /**
+ * Retrieve the current user's authentication history.
+ *
+ * @param string|null $refreshToken The refresh token of the user.
+ * @return array The list of authentication history entries.
+ * @throws AuthException|RateLimitException
+ */
+ public function history(?string $refreshToken = null): array
+ {
+ $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null;
+
+ if (!$refreshToken) {
+ throw ValidationException::forMissingRefreshToken();
+ }
+
+ return $this->api->doGet(
+ EndpointsV1::$HISTORY_PATH,
+ false,
+ $refreshToken
+ );
+ }
+
/**
* Get the Password component.
*
@@ -256,6 +342,26 @@ public function oauth(): OAuth
return $this->oauth;
}
+ /**
+ * Get the OTP component.
+ *
+ * @return OTP The OTP instance.
+ */
+ public function otp(): OTP
+ {
+ return $this->otp;
+ }
+
+ /**
+ * Get the MagicLink component.
+ *
+ * @return MagicLink The MagicLink instance.
+ */
+ public function magicLink(): MagicLink
+ {
+ return $this->magicLink;
+ }
+
/**
* Get the Management component.
*
diff --git a/src/SDK/Management/AccessKey.php b/src/SDK/Management/AccessKey.php
new file mode 100644
index 0000000..bf60233
--- /dev/null
+++ b/src/SDK/Management/AccessKey.php
@@ -0,0 +1,198 @@
+api = $api;
+ }
+
+ /**
+ * Create a new access key.
+ *
+ * @param string $name The name of the access key.
+ * @param int $expireTime Access key expiration time (in seconds since epoch). 0 means never expires.
+ * @param array $roleNames An optional list of the access key's roles without tenant association. These roles are supported for keys of a project-level access key.
+ * @param array $keyTenants An optional list of tenant associations, each an associative array like ['tenantId' => 't1', 'roleNames' => ['r1']].
+ * @param string|null $userId An optional user ID to associate the access key with. The key will inherit the user's permissions and attributes.
+ * @param array $customClaims An optional dictionary of custom claims to be added to the access key's JWT.
+ * @param string|null $description An optional description for the access key.
+ * @param array $permittedIps An optional list of IP addresses or CIDR ranges that are allowed to use this access key.
+ * @return array The created access key response, containing 'key' and 'cleartext'.
+ * @throws AuthException If the request fails.
+ */
+ public function create(
+ string $name,
+ int $expireTime = 0,
+ array $roleNames = [],
+ array $keyTenants = [],
+ ?string $userId = null,
+ array $customClaims = [],
+ ?string $description = null,
+ array $permittedIps = []
+ ): array {
+ $body = [
+ 'name' => $name,
+ 'expireTime' => $expireTime,
+ 'roleNames' => $roleNames,
+ 'keyTenants' => $keyTenants,
+ 'customClaims' => $customClaims,
+ 'permittedIps' => $permittedIps,
+ ];
+
+ if ($userId !== null) {
+ $body['userId'] = $userId;
+ }
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ return $this->api->doPost(MgmtV1::$ACCESS_KEY_CREATE_PATH, $body, true);
+ }
+
+ /**
+ * Load an existing access key by its ID.
+ *
+ * @param string $id The ID of the access key to load.
+ * @return array The access key response, containing 'key'.
+ * @throws AuthException If the request fails.
+ */
+ public function load(string $id): array
+ {
+ return $this->api->doGet(
+ MgmtV1::$ACCESS_KEY_LOAD_PATH . '?' . http_build_query(['id' => $id]),
+ true
+ );
+ }
+
+ /**
+ * Search all access keys, optionally filtering by tenant IDs.
+ *
+ * @param array $tenantIds An optional list of tenant IDs to filter the access keys by.
+ * @return array The search response, containing 'keys'.
+ * @throws AuthException If the request fails.
+ */
+ public function searchAll(array $tenantIds = []): array
+ {
+ $body = [
+ 'tenantIds' => $tenantIds,
+ ];
+
+ return $this->api->doPost(MgmtV1::$ACCESS_KEYS_SEARCH_PATH, $body, true);
+ }
+
+ /**
+ * Update an existing access key.
+ *
+ * @param string $id The ID of the access key to update.
+ * @param string $name The updated name of the access key.
+ * @param array|null $customClaims An optional dictionary of custom claims to update on the access key.
+ * @param string|null $description An optional updated description for the access key.
+ * @param array|null $roleNames An optional updated list of the access key's roles without tenant association.
+ * @param array|null $keyTenants An optional updated list of tenant associations, each an associative array like ['tenantId' => 't1', 'roleNames' => ['r1']].
+ * @return array The update response.
+ * @throws AuthException If the request fails.
+ */
+ public function update(
+ string $id,
+ string $name,
+ ?array $customClaims = null,
+ ?string $description = null,
+ ?array $roleNames = null,
+ ?array $keyTenants = null
+ ): array {
+ $body = [
+ 'id' => $id,
+ 'name' => $name,
+ ];
+
+ if ($customClaims !== null) {
+ $body['customClaims'] = $customClaims;
+ }
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+ if ($roleNames !== null) {
+ $body['roleNames'] = $roleNames;
+ }
+ if ($keyTenants !== null) {
+ $body['keyTenants'] = $keyTenants;
+ }
+
+ return $this->api->doPost(MgmtV1::$ACCESS_KEY_UPDATE_PATH, $body, true);
+ }
+
+ /**
+ * Deactivate an access key.
+ *
+ * Deactivated access keys cannot be used to authenticate but can be
+ * reactivated later using the activate method.
+ *
+ * @param string $id The ID of the access key to deactivate.
+ * @return void
+ * @throws AuthException If the request fails.
+ */
+ public function deactivate(string $id): void
+ {
+ $body = [
+ 'id' => $id,
+ ];
+
+ $this->api->doPost(MgmtV1::$ACCESS_KEY_DEACTIVATE_PATH, $body, true);
+ }
+
+ /**
+ * Activate an access key.
+ *
+ * @param string $id The ID of the access key to activate.
+ * @return void
+ * @throws AuthException If the request fails.
+ */
+ public function activate(string $id): void
+ {
+ $body = [
+ 'id' => $id,
+ ];
+
+ $this->api->doPost(MgmtV1::$ACCESS_KEY_ACTIVATE_PATH, $body, true);
+ }
+
+ /**
+ * Delete an access key.
+ *
+ * IMPORTANT: This action is irreversible. Once an access key is deleted
+ * it cannot be recovered.
+ *
+ * @param string $id The ID of the access key to delete.
+ * @return void
+ * @throws AuthException If the request fails.
+ */
+ public function delete(string $id): void
+ {
+ $body = [
+ 'id' => $id,
+ ];
+
+ $this->api->doPost(MgmtV1::$ACCESS_KEY_DELETE_PATH, $body, true);
+ }
+}
diff --git a/src/SDK/Management/Flow.php b/src/SDK/Management/Flow.php
new file mode 100644
index 0000000..e855c10
--- /dev/null
+++ b/src/SDK/Management/Flow.php
@@ -0,0 +1,172 @@
+api = $api;
+ }
+
+ /**
+ * List all project flows.
+ *
+ * This method retrieves the metadata for every flow defined in the project.
+ *
+ * @return array The response containing a 'flows' entry with the list of flows.
+ *
+ * @throws AuthException If the list operation fails.
+ */
+ public function listFlows(): array
+ {
+ $body = [];
+
+ return $this->api->doPost(
+ MgmtV1::$FLOW_LIST_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Delete flows by their IDs.
+ *
+ * This method removes all flows identified by the provided list of flow IDs.
+ *
+ * @param array $flowIds The list of flow IDs to delete.
+ *
+ * @return void
+ *
+ * @throws AuthException If the delete operation fails.
+ */
+ public function delete(array $flowIds): void
+ {
+ $body = [
+ 'ids' => $flowIds,
+ ];
+
+ $this->api->doPost(
+ MgmtV1::$FLOW_DELETE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Export a single flow by its ID.
+ *
+ * This method exports the full definition of a flow, including its screens,
+ * so it can be backed up or imported into another project.
+ *
+ * @param string $flowId The ID of the flow to export.
+ *
+ * @return array The response containing the exported flow definition.
+ *
+ * @throws AuthException If the export operation fails.
+ */
+ public function exportFlow(string $flowId): array
+ {
+ $body = [
+ 'flowId' => $flowId,
+ ];
+
+ return $this->api->doPost(
+ MgmtV1::$FLOW_EXPORT_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Import a flow into the project.
+ *
+ * This method imports a flow definition, optionally including its associated
+ * screens, under the specified flow ID.
+ *
+ * @param string $flowId The ID under which the flow will be imported.
+ * @param array $flow The flow definition to import.
+ * @param array $screens Optional list of screens associated with the flow.
+ *
+ * @return array The response containing the imported flow definition.
+ *
+ * @throws AuthException If the import operation fails.
+ */
+ public function importFlow(string $flowId, array $flow, array $screens = []): array
+ {
+ $body = [
+ 'flowId' => $flowId,
+ 'flow' => $flow,
+ 'screens' => $screens,
+ ];
+
+ return $this->api->doPost(
+ MgmtV1::$FLOW_IMPORT_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Export the project theme.
+ *
+ * This method exports the current project theme so it can be backed up or
+ * imported into another project.
+ *
+ * @return array The response containing the exported theme definition.
+ *
+ * @throws AuthException If the export operation fails.
+ */
+ public function exportTheme(): array
+ {
+ $body = [];
+
+ return $this->api->doPost(
+ MgmtV1::$THEME_EXPORT_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Import a theme into the project.
+ *
+ * This method imports a theme definition, replacing the current project theme.
+ *
+ * @param array $theme The theme definition to import.
+ *
+ * @return array The response containing the imported theme definition.
+ *
+ * @throws AuthException If the import operation fails.
+ */
+ public function importTheme(array $theme): array
+ {
+ $body = [
+ 'theme' => $theme,
+ ];
+
+ return $this->api->doPost(
+ MgmtV1::$THEME_IMPORT_PATH,
+ $body,
+ true
+ );
+ }
+}
diff --git a/src/SDK/Management/JWT.php b/src/SDK/Management/JWT.php
new file mode 100644
index 0000000..08ea696
--- /dev/null
+++ b/src/SDK/Management/JWT.php
@@ -0,0 +1,101 @@
+api = $api;
+ }
+
+ /**
+ * Update a valid JWT with the custom claims provided.
+ *
+ * The new JWT will be signed with the latest signing key. It is up to the
+ * caller to make sure the provided JWT is valid, as its claims will be used
+ * as the basis for the newly generated token.
+ *
+ * @param string $jwt The existing valid JWT to update.
+ * @param array $customClaims A map of custom claims to add to the JWT.
+ * @param int|null $refreshDuration Optional duration, in seconds, for the
+ * refreshed token. When null, it is omitted.
+ * @return string The updated, signed JWT.
+ * @throws AuthException If the update request fails.
+ */
+ public function updateJWT(string $jwt, array $customClaims = [], ?int $refreshDuration = null): string
+ {
+ $body = [
+ 'jwt' => $jwt,
+ 'customClaims' => $customClaims,
+ ];
+
+ if ($refreshDuration !== null) {
+ $body['refreshDuration'] = $refreshDuration;
+ }
+
+ $response = $this->api->doPost(MgmtV1::$UPDATE_JWT_PATH, $body, true);
+
+ return $response['jwt'] ?? '';
+ }
+
+ /**
+ * Generate a JWT for a given user, on behalf of an impersonator.
+ *
+ * The impersonator must have the required permissions in order to
+ * impersonate the target user.
+ *
+ * @param string $impersonatorId The ID of the user performing the impersonation.
+ * @param string $loginId The login ID of the user being impersonated.
+ * @param bool $validateConsent Whether to validate that consent to impersonate has been given.
+ * @param array $customClaims A map of custom claims to add to the JWT.
+ * @param string|null $selectedTenant Optional tenant to select for the impersonated session. When null, it is omitted.
+ * @param int|null $refreshDuration Optional duration, in seconds, for the refreshed token. When null, it is omitted.
+ * @return string The impersonated, signed JWT.
+ * @throws AuthException If the impersonation request fails.
+ */
+ public function impersonate(
+ string $impersonatorId,
+ string $loginId,
+ bool $validateConsent = true,
+ array $customClaims = [],
+ ?string $selectedTenant = null,
+ ?int $refreshDuration = null
+ ): string {
+ $body = [
+ 'impersonatorId' => $impersonatorId,
+ 'loginId' => $loginId,
+ 'validateConsent' => $validateConsent,
+ 'customClaims' => $customClaims,
+ ];
+
+ if ($selectedTenant !== null) {
+ $body['selectedTenant'] = $selectedTenant;
+ }
+
+ if ($refreshDuration !== null) {
+ $body['refreshDuration'] = $refreshDuration;
+ }
+
+ $response = $this->api->doPost(MgmtV1::$IMPERSONATE_PATH, $body, true);
+
+ return $response['jwt'] ?? '';
+ }
+}
diff --git a/src/SDK/Management/Management.php b/src/SDK/Management/Management.php
index f9efc36..b46c958 100644
--- a/src/SDK/Management/Management.php
+++ b/src/SDK/Management/Management.php
@@ -8,16 +8,22 @@
* Class Management
*
* Represents the management functionality for Descope, providing access to
- * user management capabilities.
+ * the full set of management components (users, tenants, roles, permissions,
+ * access keys, SSO, JWT, flows, audit and outbound apps).
*/
class Management
{
- /**
- * @var User The User management component.
- */
public User $user;
public Audit $audit;
public OutboundApps $outboundApps;
+ public Tenant $tenant;
+ public Role $role;
+ public Permission $permission;
+ public AccessKey $accessKey;
+ public SSOApplication $ssoApplication;
+ public SSOSettings $sso;
+ public JWT $jwt;
+ public Flow $flow;
/**
* Constructor for Management class.
@@ -29,6 +35,14 @@ public function __construct(API $auth)
$this->user = new User($auth);
$this->audit = new Audit($auth);
$this->outboundApps = new OutboundApps($auth);
+ $this->tenant = new Tenant($auth);
+ $this->role = new Role($auth);
+ $this->permission = new Permission($auth);
+ $this->accessKey = new AccessKey($auth);
+ $this->ssoApplication = new SSOApplication($auth);
+ $this->sso = new SSOSettings($auth);
+ $this->jwt = new JWT($auth);
+ $this->flow = new Flow($auth);
}
/**
@@ -60,4 +74,84 @@ public function outboundApps(): OutboundApps
{
return $this->outboundApps;
}
+
+ /**
+ * Get the Tenant Management component.
+ *
+ * @return Tenant The Tenant management instance.
+ */
+ public function tenant(): Tenant
+ {
+ return $this->tenant;
+ }
+
+ /**
+ * Get the Role Management component.
+ *
+ * @return Role The Role management instance.
+ */
+ public function role(): Role
+ {
+ return $this->role;
+ }
+
+ /**
+ * Get the Permission Management component.
+ *
+ * @return Permission The Permission management instance.
+ */
+ public function permission(): Permission
+ {
+ return $this->permission;
+ }
+
+ /**
+ * Get the Access Key Management component.
+ *
+ * @return AccessKey The Access Key management instance.
+ */
+ public function accessKey(): AccessKey
+ {
+ return $this->accessKey;
+ }
+
+ /**
+ * Get the SSO Application Management component.
+ *
+ * @return SSOApplication The SSO Application management instance.
+ */
+ public function ssoApplication(): SSOApplication
+ {
+ return $this->ssoApplication;
+ }
+
+ /**
+ * Get the SSO Settings Management component.
+ *
+ * @return SSOSettings The SSO Settings management instance.
+ */
+ public function sso(): SSOSettings
+ {
+ return $this->sso;
+ }
+
+ /**
+ * Get the JWT Management component.
+ *
+ * @return JWT The JWT management instance.
+ */
+ public function jwt(): JWT
+ {
+ return $this->jwt;
+ }
+
+ /**
+ * Get the Flow Management component.
+ *
+ * @return Flow The Flow management instance.
+ */
+ public function flow(): Flow
+ {
+ return $this->flow;
+ }
}
diff --git a/src/SDK/Management/MgmtV1.php b/src/SDK/Management/MgmtV1.php
index 1811dda..11cd228 100644
--- a/src/SDK/Management/MgmtV1.php
+++ b/src/SDK/Management/MgmtV1.php
@@ -20,6 +20,9 @@ class MgmtV1
// Paths for various management operations
public static string $TEMPLATE_EXPORT_PATH;
public static string $TEMPLATE_IMPORT_PATH;
+ public static string $THEME_IMPORT_PATH;
+ public static string $THEME_EXPORT_PATH;
+ public static string $FLOW_IMPORT_PATH;
public static string $FLOW_EXPORT_PATH;
public static string $FLOW_DELETE_PATH;
public static string $FLOW_LIST_PATH;
@@ -235,6 +238,9 @@ private static function updatePaths(): void
self::$FLOW_LIST_PATH = self::$baseUrl . "/v1/mgmt/flow/list";
self::$FLOW_DELETE_PATH = self::$baseUrl . "/v1/mgmt/flow/delete";
self::$FLOW_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/flow/export";
+ self::$FLOW_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/flow/import";
+ self::$THEME_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/theme/export";
+ self::$THEME_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/theme/import";
self::$TEMPLATE_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/template/import";
self::$TEMPLATE_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/template/export";
diff --git a/src/SDK/Management/Permission.php b/src/SDK/Management/Permission.php
new file mode 100644
index 0000000..ff25b52
--- /dev/null
+++ b/src/SDK/Management/Permission.php
@@ -0,0 +1,96 @@
+api = $api;
+ }
+
+ /**
+ * Create a new permission.
+ *
+ * @param string $name The name of the permission to create.
+ * @param string|null $description Optional description to explain the purpose of the permission.
+ * @return void
+ * @throws AuthException If the creation request fails.
+ */
+ public function create(string $name, ?string $description = null): void
+ {
+ $body = [
+ 'name' => $name,
+ ];
+
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ $this->api->doPost(MgmtV1::$PERMISSION_CREATE_PATH, $body, true);
+ }
+
+ /**
+ * Update an existing permission.
+ *
+ * @param string $name The name of the permission to update.
+ * @param string $newName The updated name of the permission.
+ * @param string|null $description Optional description to explain the purpose of the permission.
+ * @return void
+ * @throws AuthException If the update request fails.
+ */
+ public function update(string $name, string $newName, ?string $description = null): void
+ {
+ $body = [
+ 'name' => $name,
+ 'newName' => $newName,
+ ];
+
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ $this->api->doPost(MgmtV1::$PERMISSION_UPDATE_PATH, $body, true);
+ }
+
+ /**
+ * Delete an existing permission.
+ *
+ * @param string $name The name of the permission to delete.
+ * @return void
+ * @throws AuthException If the deletion request fails.
+ */
+ public function delete(string $name): void
+ {
+ $body = [
+ 'name' => $name,
+ ];
+
+ $this->api->doPost(MgmtV1::$PERMISSION_DELETE_PATH, $body, true);
+ }
+
+ /**
+ * Load all permissions.
+ *
+ * @return array The response containing the list of permissions under the 'permissions' key.
+ * @throws AuthException If the load request fails.
+ */
+ public function loadAll(): array
+ {
+ return $this->api->doGet(MgmtV1::$PERMISSION_LOAD_ALL_PATH, true);
+ }
+}
diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php
index 5e822be..e0227ac 100644
--- a/src/SDK/Management/Role.php
+++ b/src/SDK/Management/Role.php
@@ -20,6 +20,133 @@ public function __construct(API $api)
$this->api = $api;
}
+ /**
+ * Creates a new role.
+ *
+ * @param string $name The role name.
+ * @param string|null $description Optional role description.
+ * @param array $permissionNames Permission names to grant to the role.
+ * @param string|null $tenantId Optional tenant ID for a tenant-scoped role.
+ * @param bool $defaultRole Whether this role is a default role for new users.
+ * @return void
+ * @throws AuthException If the request fails.
+ */
+ public function create(
+ string $name,
+ ?string $description = null,
+ array $permissionNames = [],
+ ?string $tenantId = null,
+ bool $defaultRole = false
+ ): void {
+ $body = [
+ 'name' => $name,
+ 'permissionNames' => $permissionNames,
+ 'defaultRole' => $defaultRole,
+ ];
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+ if ($tenantId !== null) {
+ $body['tenantId'] = $tenantId;
+ }
+
+ $this->api->doPost(MgmtV1::$ROLE_CREATE_PATH, $body, true);
+ }
+
+ /**
+ * Updates an existing role. All parameters are required except where noted;
+ * omitted optional values will be cleared on the role.
+ *
+ * @param string $name The current role name.
+ * @param string $newName The new role name.
+ * @param string|null $description Optional role description.
+ * @param array $permissionNames Permission names to grant to the role.
+ * @param string|null $tenantId Optional tenant ID for a tenant-scoped role.
+ * @param bool $defaultRole Whether this role is a default role for new users.
+ * @return void
+ * @throws AuthException If the request fails.
+ */
+ public function update(
+ string $name,
+ string $newName,
+ ?string $description = null,
+ array $permissionNames = [],
+ ?string $tenantId = null,
+ bool $defaultRole = false
+ ): void {
+ $body = [
+ 'name' => $name,
+ 'newName' => $newName,
+ 'permissionNames' => $permissionNames,
+ 'defaultRole' => $defaultRole,
+ ];
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+ if ($tenantId !== null) {
+ $body['tenantId'] = $tenantId;
+ }
+
+ $this->api->doPost(MgmtV1::$ROLE_UPDATE_PATH, $body, true);
+ }
+
+ /**
+ * Deletes a role.
+ *
+ * @param string $name The role name.
+ * @param string|null $tenantId Optional tenant ID for a tenant-scoped role.
+ * @return void
+ * @throws AuthException If the request fails.
+ */
+ public function delete(string $name, ?string $tenantId = null): void
+ {
+ $body = ['name' => $name];
+ if ($tenantId !== null) {
+ $body['tenantId'] = $tenantId;
+ }
+
+ $this->api->doPost(MgmtV1::$ROLE_DELETE_PATH, $body, true);
+ }
+
+ /**
+ * Loads all roles in the project.
+ *
+ * @return array The response containing the list of roles.
+ * @throws AuthException If the request fails.
+ */
+ public function loadAll(): array
+ {
+ return $this->api->doGet(MgmtV1::$ROLE_LOAD_ALL_PATH, true);
+ }
+
+ /**
+ * Searches roles matching the given filters.
+ *
+ * @param array $tenantIds Filter by tenant IDs.
+ * @param array $roleNames Filter by role names.
+ * @param string|null $roleNameLike Filter by a case-insensitive partial role name.
+ * @param array $permissionNames Filter by permission names.
+ * @return array The response containing the matching roles.
+ * @throws AuthException If the request fails.
+ */
+ public function search(
+ array $tenantIds = [],
+ array $roleNames = [],
+ ?string $roleNameLike = null,
+ array $permissionNames = []
+ ): array {
+ $body = [
+ 'tenantIds' => $tenantIds,
+ 'roleNames' => $roleNames,
+ 'permissionNames' => $permissionNames,
+ ];
+ if ($roleNameLike !== null) {
+ $body['roleNameLike'] = $roleNameLike;
+ }
+
+ return $this->api->doPost(MgmtV1::$ROLE_SEARCH_PATH, $body, true);
+ }
+
/**
* Validates tenant permissions for a JWT response.
*
diff --git a/src/SDK/Management/SSOApplication.php b/src/SDK/Management/SSOApplication.php
new file mode 100644
index 0000000..f069c5a
--- /dev/null
+++ b/src/SDK/Management/SSOApplication.php
@@ -0,0 +1,384 @@
+api = $api;
+ }
+
+ /**
+ * Create a new OIDC SSO application.
+ *
+ * @param string $name The name of the SSO application.
+ * @param string $loginPageUrl The URL of the login page for this application.
+ * @param string|null $id Optional custom ID for the SSO application.
+ * @param bool $enabled Whether the application is enabled. Defaults to true.
+ * @param string|null $description Optional description of the application.
+ * @param string|null $logo Optional logo (base64 encoded image) for the application.
+ *
+ * @return array The response containing the created application's 'id'.
+ *
+ * @throws AuthException If the create operation fails.
+ */
+ public function createOidcApplication(
+ string $name,
+ string $loginPageUrl,
+ ?string $id = null,
+ bool $enabled = true,
+ ?string $description = null,
+ ?string $logo = null
+ ): array {
+ $body = [
+ 'name' => $name,
+ 'loginPageUrl' => $loginPageUrl,
+ 'enabled' => $enabled,
+ ];
+
+ if ($id !== null) {
+ $body['id'] = $id;
+ }
+
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ if ($logo !== null) {
+ $body['logo'] = $logo;
+ }
+
+ return $this->api->doPost(
+ MgmtV1::$SSO_APPLICATION_OIDC_CREATE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Create a new SAML SSO application.
+ *
+ * @param string $name The name of the SSO application.
+ * @param string $loginPageUrl The URL of the login page for this application.
+ * @param string|null $id Optional custom ID for the SSO application.
+ * @param bool $enabled Whether the application is enabled. Defaults to true.
+ * @param string|null $description Optional description of the application.
+ * @param string|null $logo Optional logo (base64 encoded image) for the application.
+ * @param bool $useMetadataInfo Whether to use metadata URL for configuration. Defaults to false.
+ * @param string|null $metadataUrl Optional SAML metadata URL (used when $useMetadataInfo is true).
+ * @param string|null $entityId Optional SAML entity ID (used when $useMetadataInfo is false).
+ * @param string|null $acsUrl Optional ACS URL (used when $useMetadataInfo is false).
+ * @param string|null $certificate Optional certificate (used when $useMetadataInfo is false).
+ * @param array $attributeMapping Attribute mapping between Descope and the application.
+ * @param array $groupsMapping Groups mapping between Descope and the application.
+ * @param array $acsAllowedCallbacks List of allowed ACS callback URLs.
+ * @param string|null $subjectNameIdType Optional subject name ID type.
+ * @param string|null $subjectNameIdFormat Optional subject name ID format.
+ * @param bool|null $defaultRelayState Optional default relay state.
+ * @param bool|null $forceAuthentication Optional flag to force authentication.
+ *
+ * @return array The response containing the created application's 'id'.
+ *
+ * @throws AuthException If the create operation fails.
+ */
+ public function createSamlApplication(
+ string $name,
+ string $loginPageUrl,
+ ?string $id = null,
+ bool $enabled = true,
+ ?string $description = null,
+ ?string $logo = null,
+ bool $useMetadataInfo = false,
+ ?string $metadataUrl = null,
+ ?string $entityId = null,
+ ?string $acsUrl = null,
+ ?string $certificate = null,
+ array $attributeMapping = [],
+ array $groupsMapping = [],
+ array $acsAllowedCallbacks = [],
+ ?string $subjectNameIdType = null,
+ ?string $subjectNameIdFormat = null,
+ ?bool $defaultRelayState = null,
+ ?bool $forceAuthentication = null
+ ): array {
+ $body = [
+ 'name' => $name,
+ 'loginPageUrl' => $loginPageUrl,
+ 'enabled' => $enabled,
+ 'useMetadataInfo' => $useMetadataInfo,
+ 'attributeMapping' => $attributeMapping,
+ 'groupsMapping' => $groupsMapping,
+ 'acsAllowedCallbacks' => $acsAllowedCallbacks,
+ ];
+
+ if ($id !== null) {
+ $body['id'] = $id;
+ }
+
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ if ($logo !== null) {
+ $body['logo'] = $logo;
+ }
+
+ if ($metadataUrl !== null) {
+ $body['metadataUrl'] = $metadataUrl;
+ }
+
+ if ($entityId !== null) {
+ $body['entityId'] = $entityId;
+ }
+
+ if ($acsUrl !== null) {
+ $body['acsUrl'] = $acsUrl;
+ }
+
+ if ($certificate !== null) {
+ $body['certificate'] = $certificate;
+ }
+
+ if ($subjectNameIdType !== null) {
+ $body['subjectNameIdType'] = $subjectNameIdType;
+ }
+
+ if ($subjectNameIdFormat !== null) {
+ $body['subjectNameIdFormat'] = $subjectNameIdFormat;
+ }
+
+ if ($defaultRelayState !== null) {
+ $body['defaultRelayState'] = $defaultRelayState;
+ }
+
+ if ($forceAuthentication !== null) {
+ $body['forceAuthentication'] = $forceAuthentication;
+ }
+
+ return $this->api->doPost(
+ MgmtV1::$SSO_APPLICATION_SAML_CREATE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Update an existing OIDC SSO application.
+ *
+ * @param string $id The ID of the SSO application to update.
+ * @param string $name The name of the SSO application.
+ * @param string $loginPageUrl The URL of the login page for this application.
+ * @param bool $enabled Whether the application is enabled. Defaults to true.
+ * @param string|null $description Optional description of the application.
+ * @param string|null $logo Optional logo (base64 encoded image) for the application.
+ *
+ * @return void
+ *
+ * @throws AuthException If the update operation fails.
+ */
+ public function updateOidcApplication(
+ string $id,
+ string $name,
+ string $loginPageUrl,
+ bool $enabled = true,
+ ?string $description = null,
+ ?string $logo = null
+ ): void {
+ $body = [
+ 'id' => $id,
+ 'name' => $name,
+ 'loginPageUrl' => $loginPageUrl,
+ 'enabled' => $enabled,
+ ];
+
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ if ($logo !== null) {
+ $body['logo'] = $logo;
+ }
+
+ $this->api->doPost(
+ MgmtV1::$SSO_APPLICATION_OIDC_UPDATE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Update an existing SAML SSO application.
+ *
+ * @param string $id The ID of the SSO application to update.
+ * @param string $name The name of the SSO application.
+ * @param string $loginPageUrl The URL of the login page for this application.
+ * @param bool $enabled Whether the application is enabled. Defaults to true.
+ * @param string|null $description Optional description of the application.
+ * @param string|null $logo Optional logo (base64 encoded image) for the application.
+ * @param bool $useMetadataInfo Whether to use metadata URL for configuration. Defaults to false.
+ * @param string|null $metadataUrl Optional SAML metadata URL (used when $useMetadataInfo is true).
+ * @param string|null $entityId Optional SAML entity ID (used when $useMetadataInfo is false).
+ * @param string|null $acsUrl Optional ACS URL (used when $useMetadataInfo is false).
+ * @param string|null $certificate Optional certificate (used when $useMetadataInfo is false).
+ * @param array $attributeMapping Attribute mapping between Descope and the application.
+ * @param array $groupsMapping Groups mapping between Descope and the application.
+ * @param array $acsAllowedCallbacks List of allowed ACS callback URLs.
+ * @param string|null $subjectNameIdType Optional subject name ID type.
+ * @param string|null $subjectNameIdFormat Optional subject name ID format.
+ * @param bool|null $defaultRelayState Optional default relay state.
+ * @param bool|null $forceAuthentication Optional flag to force authentication.
+ *
+ * @return void
+ *
+ * @throws AuthException If the update operation fails.
+ */
+ public function updateSamlApplication(
+ string $id,
+ string $name,
+ string $loginPageUrl,
+ bool $enabled = true,
+ ?string $description = null,
+ ?string $logo = null,
+ bool $useMetadataInfo = false,
+ ?string $metadataUrl = null,
+ ?string $entityId = null,
+ ?string $acsUrl = null,
+ ?string $certificate = null,
+ array $attributeMapping = [],
+ array $groupsMapping = [],
+ array $acsAllowedCallbacks = [],
+ ?string $subjectNameIdType = null,
+ ?string $subjectNameIdFormat = null,
+ ?bool $defaultRelayState = null,
+ ?bool $forceAuthentication = null
+ ): void {
+ $body = [
+ 'id' => $id,
+ 'name' => $name,
+ 'loginPageUrl' => $loginPageUrl,
+ 'enabled' => $enabled,
+ 'useMetadataInfo' => $useMetadataInfo,
+ 'attributeMapping' => $attributeMapping,
+ 'groupsMapping' => $groupsMapping,
+ 'acsAllowedCallbacks' => $acsAllowedCallbacks,
+ ];
+
+ if ($description !== null) {
+ $body['description'] = $description;
+ }
+
+ if ($logo !== null) {
+ $body['logo'] = $logo;
+ }
+
+ if ($metadataUrl !== null) {
+ $body['metadataUrl'] = $metadataUrl;
+ }
+
+ if ($entityId !== null) {
+ $body['entityId'] = $entityId;
+ }
+
+ if ($acsUrl !== null) {
+ $body['acsUrl'] = $acsUrl;
+ }
+
+ if ($certificate !== null) {
+ $body['certificate'] = $certificate;
+ }
+
+ if ($subjectNameIdType !== null) {
+ $body['subjectNameIdType'] = $subjectNameIdType;
+ }
+
+ if ($subjectNameIdFormat !== null) {
+ $body['subjectNameIdFormat'] = $subjectNameIdFormat;
+ }
+
+ if ($defaultRelayState !== null) {
+ $body['defaultRelayState'] = $defaultRelayState;
+ }
+
+ if ($forceAuthentication !== null) {
+ $body['forceAuthentication'] = $forceAuthentication;
+ }
+
+ $this->api->doPost(
+ MgmtV1::$SSO_APPLICATION_SAML_UPDATE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Delete an SSO application.
+ *
+ * @param string $id The ID of the SSO application to delete.
+ *
+ * @return void
+ *
+ * @throws AuthException If the delete operation fails.
+ */
+ public function delete(string $id): void
+ {
+ $body = [
+ 'id' => $id,
+ ];
+
+ $this->api->doPost(
+ MgmtV1::$SSO_APPLICATION_DELETE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Load an SSO application by its ID.
+ *
+ * @param string $id The ID of the SSO application to load.
+ *
+ * @return array The SSO application details.
+ *
+ * @throws AuthException If the load operation fails.
+ */
+ public function load(string $id): array
+ {
+ return $this->api->doGet(
+ MgmtV1::$SSO_APPLICATION_LOAD_PATH . '?' . http_build_query(['id' => $id]),
+ true
+ );
+ }
+
+ /**
+ * Load all SSO applications.
+ *
+ * @return array The response containing all SSO applications under the 'apps' key.
+ *
+ * @throws AuthException If the load operation fails.
+ */
+ public function loadAll(): array
+ {
+ return $this->api->doGet(
+ MgmtV1::$SSO_APPLICATION_LOAD_ALL_PATH,
+ true
+ );
+ }
+}
diff --git a/src/SDK/Management/SSOSettings.php b/src/SDK/Management/SSOSettings.php
new file mode 100644
index 0000000..d7adf54
--- /dev/null
+++ b/src/SDK/Management/SSOSettings.php
@@ -0,0 +1,176 @@
+api = $api;
+ }
+
+ /**
+ * Load the SSO settings for a tenant.
+ *
+ * This method retrieves the current SSO configuration for the given tenant.
+ *
+ * @param string $tenantId The ID of the tenant whose SSO settings are loaded.
+ *
+ * @return array The tenant's SSO settings.
+ *
+ * @throws AuthException If the load operation fails.
+ */
+ public function loadSettings(string $tenantId): array
+ {
+ return $this->api->doGet(
+ MgmtV1::$SSO_LOAD_SETTINGS_PATH . '?' . http_build_query(['tenantId' => $tenantId]),
+ true
+ );
+ }
+
+ /**
+ * Configure the OIDC SSO settings for a tenant.
+ *
+ * This method overwrites the tenant's SSO configuration with the provided
+ * OIDC settings.
+ *
+ * @param string $tenantId The ID of the tenant to configure.
+ * @param array $settings The OIDC settings (assoc array with keys such as
+ * name, clientId, clientSecret, redirectUrl, authUrl,
+ * tokenUrl, userDataUrl, scope, etc.). Passed through as-is.
+ * @param array $domains Optional list of domains associated with the tenant.
+ *
+ * @return void
+ *
+ * @throws AuthException If the configure operation fails.
+ */
+ public function configureOIDCSettings(string $tenantId, array $settings, array $domains = []): void
+ {
+ $body = [
+ 'tenantId' => $tenantId,
+ 'settings' => $settings,
+ 'domains' => $domains,
+ ];
+
+ $this->api->doPost(
+ MgmtV1::$SSO_CONFIGURE_OIDC_SETTINGS,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Configure the SAML SSO settings for a tenant.
+ *
+ * This method overwrites the tenant's SSO configuration with the provided
+ * SAML settings.
+ *
+ * @param string $tenantId The ID of the tenant to configure.
+ * @param array $settings The SAML settings. Passed through as-is.
+ * @param string|null $redirectUrl Optional redirect URL for the SSO flow.
+ * @param array $domains Optional list of domains associated with the tenant.
+ *
+ * @return void
+ *
+ * @throws AuthException If the configure operation fails.
+ */
+ public function configureSAMLSettings(
+ string $tenantId,
+ array $settings,
+ ?string $redirectUrl = null,
+ array $domains = []
+ ): void {
+ $body = [
+ 'tenantId' => $tenantId,
+ 'settings' => $settings,
+ 'domains' => $domains,
+ ];
+
+ if ($redirectUrl !== null) {
+ $body['redirectUrl'] = $redirectUrl;
+ }
+
+ $this->api->doPost(
+ MgmtV1::$SSO_CONFIGURE_SAML_SETTINGS,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Configure the SAML SSO settings for a tenant using metadata.
+ *
+ * This method overwrites the tenant's SSO configuration with the provided
+ * SAML settings supplied via metadata.
+ *
+ * @param string $tenantId The ID of the tenant to configure.
+ * @param array $settings The SAML metadata settings. Passed through as-is.
+ * @param string|null $redirectUrl Optional redirect URL for the SSO flow.
+ * @param array $domains Optional list of domains associated with the tenant.
+ *
+ * @return void
+ *
+ * @throws AuthException If the configure operation fails.
+ */
+ public function configureSAMLSettingsByMetadata(
+ string $tenantId,
+ array $settings,
+ ?string $redirectUrl = null,
+ array $domains = []
+ ): void {
+ $body = [
+ 'tenantId' => $tenantId,
+ 'settings' => $settings,
+ 'domains' => $domains,
+ ];
+
+ if ($redirectUrl !== null) {
+ $body['redirectUrl'] = $redirectUrl;
+ }
+
+ $this->api->doPost(
+ MgmtV1::$SSO_CONFIGURE_SAML_BY_METADATA_SETTINGS,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Delete the SSO settings for a tenant.
+ *
+ * This method removes the SSO configuration for the given tenant.
+ *
+ * @param string $tenantId The ID of the tenant whose SSO settings are deleted.
+ *
+ * @return void
+ *
+ * @throws AuthException If the delete operation fails.
+ */
+ public function deleteSettings(string $tenantId): void
+ {
+ $this->api->doDelete(
+ MgmtV1::$SSO_SETTINGS_PATH . '?' . http_build_query(['tenantId' => $tenantId])
+ );
+ }
+}
diff --git a/src/SDK/Management/Tenant.php b/src/SDK/Management/Tenant.php
new file mode 100644
index 0000000..a99ff93
--- /dev/null
+++ b/src/SDK/Management/Tenant.php
@@ -0,0 +1,210 @@
+api = $api;
+ }
+
+ /**
+ * Create a new tenant.
+ *
+ * This method creates a new tenant with the specified name and optional settings.
+ * If no ID is provided, one will be generated automatically.
+ *
+ * @param string $name The name of the tenant.
+ * @param string|null $id Optional custom ID for the tenant.
+ * @param array $selfProvisioningDomains Optional list of domains that can self-provision into the tenant.
+ * @param array|null $customAttributes Optional map of custom attributes for the tenant.
+ *
+ * @return array The create response containing the tenant 'id'.
+ *
+ * @throws AuthException If the create operation fails.
+ */
+ public function create(
+ string $name,
+ ?string $id = null,
+ array $selfProvisioningDomains = [],
+ ?array $customAttributes = null
+ ): array {
+ $body = [
+ 'name' => $name,
+ 'selfProvisioningDomains' => $selfProvisioningDomains,
+ ];
+
+ if ($id !== null) {
+ $body['id'] = $id;
+ }
+
+ if ($customAttributes !== null) {
+ $body['customAttributes'] = $customAttributes;
+ }
+
+ return $this->api->doPost(
+ MgmtV1::$TENANT_CREATE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Update an existing tenant.
+ *
+ * This method overwrites the tenant's settings with the provided values.
+ * All fields will be updated, so unspecified optional values should be
+ * provided in full to avoid unintentionally clearing them.
+ *
+ * @param string $id The ID of the tenant to update.
+ * @param string $name The new name of the tenant.
+ * @param array $selfProvisioningDomains Optional list of domains that can self-provision into the tenant.
+ * @param array|null $customAttributes Optional map of custom attributes for the tenant.
+ *
+ * @return void
+ *
+ * @throws AuthException If the update operation fails.
+ */
+ public function update(
+ string $id,
+ string $name,
+ array $selfProvisioningDomains = [],
+ ?array $customAttributes = null
+ ): void {
+ $body = [
+ 'id' => $id,
+ 'name' => $name,
+ 'selfProvisioningDomains' => $selfProvisioningDomains,
+ ];
+
+ if ($customAttributes !== null) {
+ $body['customAttributes'] = $customAttributes;
+ }
+
+ $this->api->doPost(
+ MgmtV1::$TENANT_UPDATE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Delete a tenant.
+ *
+ * This method removes a tenant identified by its ID. When cascade is enabled,
+ * users and other entities associated only with this tenant are also deleted.
+ *
+ * @param string $id The ID of the tenant to delete.
+ * @param bool $cascade Whether to cascade the deletion to associated entities.
+ *
+ * @return void
+ *
+ * @throws AuthException If the delete operation fails.
+ */
+ public function delete(string $id, bool $cascade = false): void
+ {
+ $body = [
+ 'id' => $id,
+ 'cascade' => $cascade,
+ ];
+
+ $this->api->doPost(
+ MgmtV1::$TENANT_DELETE_PATH,
+ $body,
+ true
+ );
+ }
+
+ /**
+ * Load a single tenant by its ID.
+ *
+ * This method retrieves the details of a specific tenant.
+ *
+ * @param string $id The ID of the tenant to load.
+ *
+ * @return array The tenant details.
+ *
+ * @throws AuthException If the load operation fails.
+ */
+ public function load(string $id): array
+ {
+ return $this->api->doGet(
+ MgmtV1::$TENANT_LOAD_PATH . '?' . http_build_query(['id' => $id]),
+ true
+ );
+ }
+
+ /**
+ * Load all tenants in the project.
+ *
+ * This method retrieves the details of every tenant configured in the project.
+ *
+ * @return array The response containing the list of 'tenants'.
+ *
+ * @throws AuthException If the load operation fails.
+ */
+ public function loadAll(): array
+ {
+ return $this->api->doGet(
+ MgmtV1::$TENANT_LOAD_ALL_PATH,
+ true
+ );
+ }
+
+ /**
+ * Search for tenants matching the provided filters.
+ *
+ * This method searches all tenants, optionally filtering by IDs, names,
+ * self-provisioning domains, and custom attributes.
+ *
+ * @param array $ids Optional list of tenant IDs to filter by.
+ * @param array $names Optional list of tenant names to filter by.
+ * @param array $selfProvisioningDomains Optional list of self-provisioning domains to filter by.
+ * @param array|null $customAttributes Optional map of custom attributes to filter by.
+ *
+ * @return array The search response containing the matching tenants.
+ *
+ * @throws AuthException If the search operation fails.
+ */
+ public function searchAll(
+ array $ids = [],
+ array $names = [],
+ array $selfProvisioningDomains = [],
+ ?array $customAttributes = null
+ ): array {
+ $body = [
+ 'tenantIds' => $ids,
+ 'tenantNames' => $names,
+ 'tenantSelfProvisioningDomains' => $selfProvisioningDomains,
+ ];
+
+ if ($customAttributes !== null) {
+ $body['customAttributes'] = $customAttributes;
+ }
+
+ return $this->api->doPost(
+ MgmtV1::$TENANT_SEARCH_ALL_PATH,
+ $body,
+ true
+ );
+ }
+}
diff --git a/src/tests/Auth/AuthParityTest.php b/src/tests/Auth/AuthParityTest.php
new file mode 100644
index 0000000..4d78361
--- /dev/null
+++ b/src/tests/Auth/AuthParityTest.php
@@ -0,0 +1,125 @@
+sdk = new DescopeSDK(['projectId' => 'Ptest']);
+ EndpointsV1::setBaseUrlFromString('https://api.descope.com');
+ }
+
+ public function testAuthComponentsAreWired(): void
+ {
+ $this->assertInstanceOf(OTP::class, $this->sdk->otp);
+ $this->assertInstanceOf(OTP::class, $this->sdk->otp());
+ $this->assertInstanceOf(MagicLink::class, $this->sdk->magicLink);
+ $this->assertInstanceOf(MagicLink::class, $this->sdk->magicLink());
+ }
+
+ /**
+ * @dataProvider sessionMethodsProvider
+ */
+ public function testSessionLifecycleMethodsExist(string $method): void
+ {
+ $this->assertTrue(
+ method_exists($this->sdk, $method),
+ sprintf('DescopeSDK is missing method %s()', $method)
+ );
+ }
+
+ public function sessionMethodsProvider(): array
+ {
+ return [
+ ['logout'],
+ ['logoutAll'],
+ ['selectTenant'],
+ ['exchangeAccessKey'],
+ ['history'],
+ ['refreshSession'],
+ ];
+ }
+
+ public function testOtpSignInAppendsMethodAndSendsLoginId(): void
+ {
+ $api = $this->createMock(API::class);
+ $api->expects($this->once())
+ ->method('doPost')
+ ->with(
+ $this->stringEndsWith('/v1/auth/otp/signin/email'),
+ ['loginId' => 'a@b.com'],
+ false,
+ null
+ )
+ ->willReturn(['email' => 'a***@b.com']);
+
+ $otp = new OTP($api);
+ $this->assertSame('a***@b.com', $otp->signIn('email', 'a@b.com'));
+ }
+
+ public function testOtpVerifyReturnsJwtResponse(): void
+ {
+ $api = $this->createMock(API::class);
+ $api->expects($this->once())
+ ->method('doPost')
+ ->with(
+ $this->stringEndsWith('/v1/auth/otp/verify/sms'),
+ ['loginId' => '+123', 'code' => '000000'],
+ false
+ )
+ ->willReturn(['refreshJwt' => 'rjwt']);
+ $api->expects($this->once())
+ ->method('generateJwtResponse')
+ ->willReturn(['sessionToken' => 'st']);
+
+ $otp = new OTP($api);
+ $this->assertSame(['sessionToken' => 'st'], $otp->verifyCode('sms', '+123', '000000'));
+ }
+
+ public function testOtpRejectsUnknownMethod(): void
+ {
+ $this->expectException(AuthException::class);
+ $this->expectExceptionMessage('Unknown delivery method');
+ (new OTP($this->createMock(API::class)))->signIn('carrier-pigeon', 'a@b.com');
+ }
+
+ public function testMagicLinkSignUpIncludesUri(): void
+ {
+ $api = $this->createMock(API::class);
+ $api->expects($this->once())
+ ->method('doPost')
+ ->with(
+ $this->stringEndsWith('/v1/auth/magiclink/signup/email'),
+ $this->callback(fn ($body) => $body['loginId'] === 'a@b.com' && $body['uri'] === 'https://app/verify'),
+ false
+ )
+ ->willReturn(['email' => 'a***@b.com']);
+
+ $ml = new MagicLink($api);
+ $this->assertSame('a***@b.com', $ml->signUp('email', 'a@b.com', 'https://app/verify'));
+ }
+
+ public function testMagicLinkVerifyRejectsEmptyToken(): void
+ {
+ $this->expectException(AuthException::class);
+ $this->expectExceptionMessage('token cannot be empty');
+ (new MagicLink($this->createMock(API::class)))->verify('');
+ }
+}
diff --git a/src/tests/Management/AccessKeyTest.php b/src/tests/Management/AccessKeyTest.php
new file mode 100644
index 0000000..32a3470
--- /dev/null
+++ b/src/tests/Management/AccessKeyTest.php
@@ -0,0 +1,56 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testCreate()
+ {
+ $result = $this->descopeSDK->management->accessKey->create('My Key');
+ $this->assertIsArray($result);
+ }
+
+ public function testSearchAll()
+ {
+ $result = $this->descopeSDK->management->accessKey->searchAll();
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('keys', $result);
+ }
+
+ public function testLoadUpdate()
+ {
+ $this->descopeSDK->management->accessKey->load('k1');
+ $this->descopeSDK->management->accessKey->update('k1', 'Renamed');
+ $this->assertTrue(true);
+ }
+
+ public function testActivateDeactivateDelete()
+ {
+ $this->descopeSDK->management->accessKey->activate('k1');
+ $this->descopeSDK->management->accessKey->deactivate('k1');
+ $this->descopeSDK->management->accessKey->delete('k1');
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/tests/Management/FlowTest.php b/src/tests/Management/FlowTest.php
new file mode 100644
index 0000000..775ca8d
--- /dev/null
+++ b/src/tests/Management/FlowTest.php
@@ -0,0 +1,59 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testListFlows()
+ {
+ $result = $this->descopeSDK->management->flow->listFlows();
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('flows', $result);
+ }
+
+ public function testExportImportFlow()
+ {
+ $exported = $this->descopeSDK->management->flow->exportFlow('sign-up-or-in');
+ $this->assertIsArray($exported);
+
+ $imported = $this->descopeSDK->management->flow->importFlow('sign-up-or-in', []);
+ $this->assertIsArray($imported);
+ }
+
+ public function testDelete()
+ {
+ $this->descopeSDK->management->flow->delete(['f1']);
+ $this->assertTrue(true);
+ }
+
+ public function testExportImportTheme()
+ {
+ $exported = $this->descopeSDK->management->flow->exportTheme();
+ $this->assertIsArray($exported);
+
+ $imported = $this->descopeSDK->management->flow->importTheme([]);
+ $this->assertIsArray($imported);
+ }
+}
diff --git a/src/tests/Management/JWTTest.php b/src/tests/Management/JWTTest.php
new file mode 100644
index 0000000..95cbbdf
--- /dev/null
+++ b/src/tests/Management/JWTTest.php
@@ -0,0 +1,40 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testUpdateJWT()
+ {
+ $result = $this->descopeSDK->management->jwt->updateJWT('someJwt', ['k' => 'v']);
+ $this->assertIsString($result);
+ }
+
+ public function testImpersonate()
+ {
+ $result = $this->descopeSDK->management->jwt->impersonate('imp1', 'login1');
+ $this->assertIsString($result);
+ }
+}
diff --git a/src/tests/Management/ManagementParityTest.php b/src/tests/Management/ManagementParityTest.php
new file mode 100644
index 0000000..084c278
--- /dev/null
+++ b/src/tests/Management/ManagementParityTest.php
@@ -0,0 +1,83 @@
+ 'Ptest',
+ 'managementKey' => 'Ktest',
+ ]);
+ $this->management = $sdk->management;
+ }
+
+ public function testAllComponentsAreWired(): void
+ {
+ $components = [
+ 'user' => \Descope\SDK\Management\User::class,
+ 'audit' => \Descope\SDK\Management\Audit::class,
+ 'outboundApps' => \Descope\SDK\Management\OutboundApps::class,
+ 'tenant' => \Descope\SDK\Management\Tenant::class,
+ 'role' => \Descope\SDK\Management\Role::class,
+ 'permission' => \Descope\SDK\Management\Permission::class,
+ 'accessKey' => \Descope\SDK\Management\AccessKey::class,
+ 'ssoApplication' => \Descope\SDK\Management\SSOApplication::class,
+ 'sso' => \Descope\SDK\Management\SSOSettings::class,
+ 'jwt' => \Descope\SDK\Management\JWT::class,
+ 'flow' => \Descope\SDK\Management\Flow::class,
+ ];
+
+ foreach ($components as $property => $class) {
+ $this->assertInstanceOf($class, $this->management->$property);
+ $this->assertInstanceOf($class, $this->management->$property());
+ }
+ }
+
+ /**
+ * @dataProvider expectedMethodsProvider
+ */
+ public function testComponentExposesMethods(string $property, array $methods): void
+ {
+ $component = $this->management->$property;
+ foreach ($methods as $method) {
+ $this->assertTrue(
+ method_exists($component, $method),
+ sprintf('%s is missing method %s()', get_class($component), $method)
+ );
+ }
+ }
+
+ public function expectedMethodsProvider(): array
+ {
+ return [
+ 'tenant' => ['tenant', ['create', 'update', 'delete', 'load', 'loadAll', 'searchAll']],
+ 'role' => ['role', ['create', 'update', 'delete', 'loadAll', 'search']],
+ 'permission' => ['permission', ['create', 'update', 'delete', 'loadAll']],
+ 'accessKey' => ['accessKey', ['create', 'load', 'searchAll', 'update', 'activate', 'deactivate', 'delete']],
+ 'ssoApplication' => ['ssoApplication', [
+ 'createOidcApplication', 'createSamlApplication',
+ 'updateOidcApplication', 'updateSamlApplication',
+ 'delete', 'load', 'loadAll',
+ ]],
+ 'sso' => ['sso', [
+ 'loadSettings', 'configureOIDCSettings', 'configureSAMLSettings',
+ 'configureSAMLSettingsByMetadata', 'deleteSettings',
+ ]],
+ 'jwt' => ['jwt', ['updateJWT', 'impersonate']],
+ 'flow' => ['flow', ['listFlows', 'delete', 'exportFlow', 'importFlow', 'exportTheme', 'importTheme']],
+ ];
+ }
+}
diff --git a/src/tests/Management/PermissionTest.php b/src/tests/Management/PermissionTest.php
new file mode 100644
index 0000000..99f3abe
--- /dev/null
+++ b/src/tests/Management/PermissionTest.php
@@ -0,0 +1,48 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testCreate()
+ {
+ $this->descopeSDK->management->permission->create('Read', 'desc');
+ $this->assertTrue(true);
+ }
+
+ public function testLoadAll()
+ {
+ $result = $this->descopeSDK->management->permission->loadAll();
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('permissions', $result);
+ }
+
+ public function testUpdateDelete()
+ {
+ $this->descopeSDK->management->permission->update('Read', 'ReadOnly');
+ $this->descopeSDK->management->permission->delete('ReadOnly');
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/tests/Management/RoleTest.php b/src/tests/Management/RoleTest.php
new file mode 100644
index 0000000..39ec7a6
--- /dev/null
+++ b/src/tests/Management/RoleTest.php
@@ -0,0 +1,53 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testCreate()
+ {
+ $this->descopeSDK->management->role->create('My Role', 'desc', ['Read']);
+ $this->assertTrue(true);
+ }
+
+ public function testLoadAll()
+ {
+ $result = $this->descopeSDK->management->role->loadAll();
+ $this->assertIsArray($result);
+ }
+
+ public function testSearch()
+ {
+ $result = $this->descopeSDK->management->role->search();
+ $this->assertIsArray($result);
+ }
+
+ public function testUpdateDelete()
+ {
+ $this->descopeSDK->management->role->update('My Role', 'Renamed');
+ $this->descopeSDK->management->role->delete('Renamed');
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/tests/Management/SSOApplicationTest.php b/src/tests/Management/SSOApplicationTest.php
new file mode 100644
index 0000000..85a0571
--- /dev/null
+++ b/src/tests/Management/SSOApplicationTest.php
@@ -0,0 +1,63 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testCreateOidcApplication()
+ {
+ $result = $this->descopeSDK->management->ssoApplication->createOidcApplication(
+ 'My App',
+ 'https://login.example.com'
+ );
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('id', $result);
+ }
+
+ public function testCreateSamlApplication()
+ {
+ $result = $this->descopeSDK->management->ssoApplication->createSamlApplication(
+ 'My SAML',
+ 'https://login.example.com'
+ );
+
+ $this->assertIsArray($result);
+ }
+
+ public function testLoadAll()
+ {
+ $result = $this->descopeSDK->management->ssoApplication->loadAll();
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('apps', $result);
+ }
+
+ public function testLoadDelete()
+ {
+ $this->descopeSDK->management->ssoApplication->load('app1');
+ $this->descopeSDK->management->ssoApplication->delete('app1');
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/tests/Management/SSOSettingsTest.php b/src/tests/Management/SSOSettingsTest.php
new file mode 100644
index 0000000..9f3b19d
--- /dev/null
+++ b/src/tests/Management/SSOSettingsTest.php
@@ -0,0 +1,60 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testLoadSettings()
+ {
+ $result = $this->descopeSDK->management->sso->loadSettings('t1');
+ $this->assertIsArray($result);
+ }
+
+ public function testConfigureOIDCSettings()
+ {
+ $this->descopeSDK->management->sso->configureOIDCSettings('t1', [
+ 'name' => 'n',
+ 'clientId' => 'c',
+ 'clientSecret' => 's',
+ ]);
+ $this->assertTrue(true);
+ }
+
+ public function testConfigureSAMLSettings()
+ {
+ $this->descopeSDK->management->sso->configureSAMLSettings('t1', [
+ 'idpUrl' => 'u',
+ 'entityId' => 'e',
+ 'idpCert' => 'cert',
+ ]);
+ $this->assertTrue(true);
+ }
+
+ public function testDeleteSettings()
+ {
+ $this->descopeSDK->management->sso->deleteSettings('t1');
+ $this->assertTrue(true);
+ }
+}
diff --git a/src/tests/Management/TenantTest.php b/src/tests/Management/TenantTest.php
new file mode 100644
index 0000000..9c78e23
--- /dev/null
+++ b/src/tests/Management/TenantTest.php
@@ -0,0 +1,56 @@
+markTestSkipped('Management integration tests require DESCOPE_PROJECT_ID and DESCOPE_MANAGEMENT_KEY in env.');
+ }
+
+ $config = [
+ 'projectId' => $projectId,
+ 'managementKey' => $managementKey,
+ ];
+
+ $this->descopeSDK = new DescopeSDK($config);
+ }
+
+ public function testCreate()
+ {
+ $result = $this->descopeSDK->management->tenant->create('My Tenant');
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('id', $result);
+ }
+
+ public function testLoadAll()
+ {
+ $result = $this->descopeSDK->management->tenant->loadAll();
+ $this->assertIsArray($result);
+ }
+
+ public function testSearchAll()
+ {
+ $result = $this->descopeSDK->management->tenant->searchAll();
+ $this->assertIsArray($result);
+ }
+
+ public function testLoadUpdateDelete()
+ {
+ $this->descopeSDK->management->tenant->load('t1');
+ $this->descopeSDK->management->tenant->update('t1', 'New');
+ $this->descopeSDK->management->tenant->delete('t1');
+ $this->assertTrue(true);
+ }
+}