From a7648fa5277be520fc1d21a659b9847bf936e3ab Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Thu, 6 Jul 2023 10:23:18 -0700 Subject: [PATCH 01/13] RELEASE --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92ed7c3..78459e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,8 +32,7 @@ jobs: minor-wording: "MINOR" env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - - name: cat package.json - run: cat ./package.json + - name: Output Step env: NEW_TAG: ${{ steps.version-bump.outputs.newTag }} From f1c2b42baa5d4b06ddadf6f83c2192ff8b59597f Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Mon, 21 Oct 2024 23:35:45 -0700 Subject: [PATCH 02/13] added changes --- src/SDK/API.php | 3 +- src/SDK/Management/MgmtV1.php | 80 +++++++++++++- src/SDK/Management/User.php | 4 +- src/SDK/Management/UserPassword.php | 164 +++++++++++++++++++++------- 4 files changed, 204 insertions(+), 47 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index 880efcc..93ede0c 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -27,7 +27,8 @@ class API /** * Constructor for API class. * - * @param string $managementKey Management key for authentication. + * @param string $projectId + * @param string|null $managementKey Management key for authentication. */ public function __construct(string $projectId, ?string $managementKey) { diff --git a/src/SDK/Management/MgmtV1.php b/src/SDK/Management/MgmtV1.php index d249db2..78f60f1 100644 --- a/src/SDK/Management/MgmtV1.php +++ b/src/SDK/Management/MgmtV1.php @@ -10,7 +10,84 @@ class MgmtV1 { - private static $baseUrl = DEFAULT_URL_PREFIX . '.' . DEFAULT_DOMAIN; + public static string $baseUrl = DEFAULT_URL_PREFIX . '.' . DEFAULT_DOMAIN; + public static string $TEMPLATE_EXPORT_PATH; + public static string $TEMPLATE_IMPORT_PATH; + public static string $FLOW_EXPORT_PATH; + public static string $FLOW_DELETE_PATH; + public static string $FLOW_LIST_PATH; + public static string $ROLE_SEARCH_PATH; + public static string $ROLE_LOAD_ALL_PATH; + public static string $ROLE_DELETE_PATH; + public static string $ROLE_UPDATE_PATH; + public static string $ROLE_CREATE_PATH; + public static string $PERMISSION_LOAD_ALL_PATH; + public static string $PERMISSION_DELETE_PATH; + public static string $PERMISSION_UPDATE_PATH; + public static string $PERMISSION_CREATE_PATH; + public static string $IMPERSONATE_PATH; + public static string $UPDATE_JWT_PATH; + public static string $SSO_CONFIGURE_SAML_BY_METADATA_SETTINGS; + public static string $SSO_CONFIGURE_SAML_SETTINGS; + public static string $SSO_CONFIGURE_OIDC_SETTINGS; + public static string $SSO_LOAD_SETTINGS_PATH; + public static string $SSO_MAPPING_PATH; + public static string $SSO_METADATA_PATH; + public static string $SSO_SETTINGS_PATH; + public static string $ACCESS_KEY_DELETE_PATH; + public static string $ACCESS_KEY_ACTIVATE_PATH; + public static string $ACCESS_KEY_DEACTIVATE_PATH; + public static string $ACCESS_KEY_UPDATE_PATH; + public static string $ACCESS_KEYS_SEARCH_PATH; + public static string $ACCESS_KEY_LOAD_PATH; + public static string $ACCESS_KEY_CREATE_PATH; + public static string $USER_HISTORY_PATH; + public static string $USER_GENERATE_EMBEDDED_LINK_PATH; + public static string $USER_GENERATE_ENCHANTED_LINK_FOR_TEST_PATH; + public static string $USER_GENERATE_MAGIC_LINK_FOR_TEST_PATH; + public static string $USER_GENERATE_OTP_FOR_TEST_PATH; + public static string $USER_REMOVE_TENANT_PATH; + public static string $USER_ADD_TENANT_PATH; + public static string $USER_REMOVE_ALL_PASSKEYS_PATH; + public static string $USER_EXPIRE_PASSWORD_PATH; + public static string $USER_SET_ACTIVE_PASSWORD_PATH; + public static string $USER_SET_TEMPORARY_PASSWORD_PATH; + public static string $USER_SET_PASSWORD_PATH; + public static string $USER_REMOVE_SSO_APPS; + public static string $USER_SET_SSO_APPS; + public static string $USER_ADD_SSO_APPS; + public static string $USER_REMOVE_ROLE_PATH; + public static string $USER_ADD_ROLE_PATH; + public static string $USER_SET_ROLE_PATH; + public static string $USER_UPDATE_CUSTOM_ATTRIBUTE_PATH; + public static string $USER_UPDATE_PICTURE_PATH; + public static string $USER_UPDATE_NAME_PATH; + public static string $USER_UPDATE_PHONE_PATH; + public static string $USER_UPDATE_EMAIL_PATH; + public static string $USER_UPDATE_LOGIN_ID_PATH; + public static string $USER_UPDATE_STATUS_PATH; + public static string $USER_GET_PROVIDER_TOKEN; + public static string $USERS_SEARCH_PATH; + public static string $USER_LOAD_PATH; + public static string $USER_DELETE_ALL_TEST_USERS_PATH; + public static string $USER_LOGOUT_PATH; + public static string $USER_DELETE_PATH; + public static string $USER_UPDATE_PATH; + public static string $USER_CREATE_BATCH_PATH; + public static string $USER_CREATE_PATH; + public static string $SSO_APPLICATION_LOAD_ALL_PATH; + public static string $SSO_APPLICATION_LOAD_PATH; + public static string $SSO_APPLICATION_DELETE_PATH; + public static string $SSO_APPLICATION_SAML_UPDATE_PATH; + public static string $SSO_APPLICATION_OIDC_UPDATE_PATH; + public static string $SSO_APPLICATION_SAML_CREATE_PATH; + public static string $SSO_APPLICATION_OIDC_CREATE_PATH; + public static string $TENANT_SEARCH_ALL_PATH; + public static string $TENANT_LOAD_ALL_PATH; + public static string $TENANT_LOAD_PATH; + public static string $TENANT_DELETE_PATH; + public static string $TENANT_UPDATE_PATH; + public static string $TENANT_CREATE_PATH; public static function setBaseUrl(string $projectId): void { @@ -31,6 +108,7 @@ private static function extractRegionFromProjectId(string $projectId): ?string $region = substr($projectId, 1, 5); return !empty($region) ? $region : null; } + return null; } private static function updatePaths(): void diff --git a/src/SDK/Management/User.php b/src/SDK/Management/User.php index efcdb32..699879f 100644 --- a/src/SDK/Management/User.php +++ b/src/SDK/Management/User.php @@ -1285,8 +1285,6 @@ public function composeCreateBody( } } - print_r($res); - return $res; } @@ -1361,7 +1359,7 @@ public function composeUpdateBody( 'additionalLoginIds' => $additionalLoginIds, 'ssoAppIds' => $ssoAppIds, ]; - print_r($customAttributes); + if ($verifiedEmail !== null) { $res['verifiedEmail'] = $verifiedEmail; } diff --git a/src/SDK/Management/UserPassword.php b/src/SDK/Management/UserPassword.php index c2612f7..c07c858 100644 --- a/src/SDK/Management/UserPassword.php +++ b/src/SDK/Management/UserPassword.php @@ -5,26 +5,40 @@ namespace Descope\SDK\Management; +/** + * Class UserPasswordPHPass + * + * Represents a user password hashed using the PHPass algorithm. + * This includes the hash, salt, and iterations used to generate the hash. + */ class UserPasswordPHPass { - private string $hash; - private string $salt; - private int $iterations; + public string $hash; + public string $salt; + public int $iterations; + /** + * Constructor to initialize PHPass password details. + * + * @param string $hash Base64-encoded password hash. + * @param string $salt Base64-encoded salt value. + * @param int $iterations The number of iterations. + */ public function __construct( string $hash, string $salt, int $iterations ) { - /** - * The hash and salt should be base64 strings using standard encoding with padding. - * The iterations cost value is an integer, usually in the thousands. - */ $this->hash = $hash; $this->salt = $salt; $this->iterations = $iterations; } + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ public function toArray(): array { return [ @@ -37,18 +51,30 @@ public function toArray(): array } } +/** + * Class UserPasswordBcrypt + * + * Represents a user password hashed using the bcrypt algorithm. + */ class UserPasswordBcrypt { - private string $hash; + public string $hash; + /** + * Constructor to initialize Bcrypt password details. + * + * @param string $hash The bcrypt hash in plaintext format (e.g., "$2a$..."). + */ public function __construct(string $hash) { - /** - * The bcrypt hash in plaintext format, for example "$2a$..." - */ $this->hash = $hash; } + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ public function toArray(): array { return [ @@ -59,15 +85,30 @@ public function toArray(): array } } +/** + * Class UserPasswordFirebase + * + * Represents a user password hashed using Firebase's custom hashing scheme. + */ class UserPasswordFirebase { - private string $hash; - private string $salt; - private string $saltSeparator; - private string $signerKey; - private int $memory; - private int $rounds; + public string $hash; + public string $salt; + public string $saltSeparator; + public string $signerKey; + public int $memory; + public int $rounds; + /** + * Constructor to initialize Firebase password details. + * + * @param string $hash Base64-encoded hash. + * @param string $salt Base64-encoded salt. + * @param string $saltSeparator Base64-encoded salt separator. + * @param string $signerKey Base64-encoded signer key. + * @param int $memory Memory cost (between 12 and 17). + * @param int $rounds Rounds cost (between 6 and 10). + */ public function __construct( string $hash, string $salt, @@ -76,12 +117,6 @@ public function __construct( int $memory, int $rounds ) { - /** - * The hash, salt, salt separator, and signer key should be base64 strings using - * standard encoding with padding. - * The memory cost value is an integer, usually between 12 to 17. - * The rounds cost value is an integer, usually between 6 to 10. - */ $this->hash = $hash; $this->salt = $salt; $this->saltSeparator = $saltSeparator; @@ -90,6 +125,11 @@ public function __construct( $this->rounds = $rounds; } + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ public function toArray(): array { return [ @@ -105,30 +145,44 @@ public function toArray(): array } } + +/** + * Class UserPasswordPbkdf2 + * + * Represents a user password hashed using the PBKDF2 algorithm. + */ class UserPasswordPbkdf2 { - private string $hash; - private string $salt; - private int $iterations; - private string $variant; + public string $hash; + public string $salt; + public int $iterations; + public string $variant; + /** + * Constructor to initialize PBKDF2 password details. + * + * @param string $hash Base64-encoded hash. + * @param string $salt Base64-encoded salt. + * @param int $iterations Number of iterations (usually in the thousands). + * @param string $variant Hash variant (sha1, sha256, or sha512). + */ public function __construct( string $hash, string $salt, int $iterations, string $variant ) { - /** - * The hash and salt should be base64 strings using standard encoding with padding. - * The iterations cost value is an integer, usually in the thousands. - * The hash variant should be either "sha1", "sha256", or "sha512". - */ $this->hash = $hash; $this->salt = $salt; $this->iterations = $iterations; $this->variant = $variant; } + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ public function toArray(): array { return [ @@ -142,18 +196,30 @@ public function toArray(): array } } +/** + * Class UserPasswordDjango + * + * Represents a user password hashed using Django's custom hashing scheme. + */ class UserPasswordDjango { - private string $hash; + public string $hash; + /** + * Constructor to initialize Django password details. + * + * @param string $hash The django hash in plaintext format (e.g., "pbkdf2_sha256$..."). + */ public function __construct(string $hash) { - /** - * The django hash in plaintext format, for example "pbkdf2_sha256$..." - */ $this->hash = $hash; } + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ public function toArray(): array { return [ @@ -164,21 +230,35 @@ public function toArray(): array } } +/** + * Class UserPassword + * + * Represents either a cleartext password or a hashed password. + * This is used when creating or inviting users with a password. + */ class UserPassword { - private ?string $cleartext; - private ?object $hashed; + public ?string $cleartext; + public ?object $hashed; + /** + * Constructor to initialize password details. + * Either cleartext or hashed password should be provided, not both. + * + * @param string|null $cleartext Plaintext password. + * @param object|null $hashed Hashed password object (one of the above classes). + */ public function __construct(?string $cleartext = null, ?object $hashed = null) { - /** - * Set a UserPassword on UserObj objects when calling invite_batch to create or invite users - * with a cleartext or prehashed password. Note that only one of the two options should be set. - */ $this->cleartext = $cleartext; $this->hashed = $hashed; } + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ public function toArray(): array { $data = []; From d756800cc9e6e7fa9259b5e14a3437df09a9ad5a Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Tue, 22 Oct 2024 00:27:58 -0700 Subject: [PATCH 03/13] added files --- src/SDK/API.php | 4 +++- src/SDK/Management/User.php | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index 93ede0c..d95089d 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -51,9 +51,11 @@ private function transformEmptyArraysToObjects($data) if (is_array($data)) { foreach ($data as &$value) { if (is_array($value)) { + // If the array is empty, ensure it's preserved as an empty array if (empty($value)) { - $value = new \stdClass(); + $value = []; } else { + // Recur for non-empty arrays $value = $this->transformEmptyArraysToObjects($value); } } diff --git a/src/SDK/Management/User.php b/src/SDK/Management/User.php index 699879f..dd7f4d6 100644 --- a/src/SDK/Management/User.php +++ b/src/SDK/Management/User.php @@ -1275,6 +1275,7 @@ public function composeCreateBody( 'additionalLoginIds' => $additionalLoginIds, 'ssoAppIds' => $ssoAppIds, ]; + if ($password !== null) { if (isset($password->cleartext)) { $res['password'] = $password->cleartext; @@ -1284,7 +1285,7 @@ public function composeCreateBody( } } } - + return $res; } From 001fea4d0a91d4ffd3e5f3c0be2efa32a9560ac9 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 17:46:43 -0700 Subject: [PATCH 04/13] added changes --- sample/callback.php | 28 +- sample/dashboard.php | 27 +- sample/login.php | 17 +- src/SDK/API.php | 50 +++- src/SDK/Auth/Password.php | 13 +- src/SDK/Auth/SSO.php | 35 ++- src/SDK/DescopeSDK.php | 110 +++++-- src/SDK/EndpointsV1.php | 38 ++- src/SDK/Management/AssociatedTenant.php | 29 +- src/SDK/Management/Management.php | 11 + src/SDK/Management/MgmtV1.php | 110 +++++-- src/SDK/Management/User.php | 374 ++++++++++++------------ src/tests/Management/UserTest.php | 150 ++++------ 13 files changed, 586 insertions(+), 406 deletions(-) diff --git a/sample/callback.php b/sample/callback.php index 63b0580..c53c5c6 100644 --- a/sample/callback.php +++ b/sample/callback.php @@ -20,18 +20,26 @@ $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'] - ]); + ]); - if (isset($_POST["sessionToken"]) && $descopeSDK->verify($_POST["sessionToken"])) { - $_SESSION["user"] = json_decode($_POST["userDetails"], true); - $_SESSION["sessionToken"] = $_POST["sessionToken"]; - - session_write_close(); - - // Redirect to dashboard - header('Location: dashboard.php'); - exit(); + if (isset($_POST["sessionToken"])) { + if ($descopeSDK->verify($_POST["sessionToken"])) { + $_SESSION["user"] = json_decode($_POST["userDetails"], true); + $_SESSION["sessionToken"] = $_POST["sessionToken"]; + session_write_close(); + + // Redirect to dashboard + header('Location: dashboard.php'); + exit(); + } else { + error_log("Session token verification failed."); + $descopeSDK->logout(); + // Redirect to login page + header('Location: login.php'); + exit(); + } } else { + error_log("Session token is not set in POST request."); // Redirect to login page header('Location: login.php'); exit(); diff --git a/sample/dashboard.php b/sample/dashboard.php index a115f11..626083a 100644 --- a/sample/dashboard.php +++ b/sample/dashboard.php @@ -1,22 +1,23 @@ load(); + +if (!isset($_ENV['DESCOPE_PROJECT_ID'])) { + echo "Descope Project ID not present. Please check .env file."; + exit(1); +} - $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..'); - $dotenv->load(); +$descopeSDK = new DescopeSDK([ + 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], + 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'] +]); - if (!isset($_ENV['DESCOPE_PROJECT_ID'])) { - echo "Descope Project ID not present. Please check .env file."; - exit(1); - } - $descopeSDK = new DescopeSDK([ - 'projectId' => $_ENV['DESCOPE_PROJECT_ID'] - ]); if (!isset($_SESSION["user"])) { session_destroy(); diff --git a/sample/login.php b/sample/login.php index 3088f2c..9f177ba 100644 --- a/sample/login.php +++ b/sample/login.php @@ -39,14 +39,25 @@ function sendFormData(sessionToken, userDetails) { async function handleLogin() { try { - // Wait for refresh to ensure token validity + console.log("Attempting to refresh the session..."); await sdk.refresh(); const sessionToken = sdk.getSessionToken(); + console.log("Session token obtained:", sessionToken); + + if (!sessionToken) { + console.log("Session token is missing after refresh. Redirecting to login."); + window.location.href = 'login.php'; // Redirect to login if session token is invalid + return; + } + const user = await getUserDetails(); + console.log("User details obtained:", user); + sendFormData(sessionToken, user.data); } catch (error) { console.log("Error during login:", error); + window.location.href = 'login.php'; // Redirect to login on error } } @@ -57,15 +68,17 @@ function sendFormData(sessionToken, userDetails) { console.log("Valid refresh token found. Logging in..."); handleLogin(); } else { + console.log("No valid refresh token. Displaying login form."); const container = document.getElementById("container") container.innerHTML = ''; const wcElement = document.getElementsByTagName('descope-wc')[0]; const onSuccess = async (e) => { + console.log("Login successful, handling login."); await handleLogin(); // Wait for login and session details } - const onError = (err) => console.log(err); + const onError = (err) => console.log("Login error:", err); if (wcElement) { wcElement.addEventListener('success', onSuccess); diff --git a/src/SDK/API.php b/src/SDK/API.php index d95089d..ae56c03 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -5,6 +5,7 @@ namespace Descope\SDK; use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; use GuzzleHttp\Exception\RequestException; use Descope\SDK\Exception\AuthException; use Descope\SDK\EndpointsV1; @@ -33,6 +34,22 @@ class API public function __construct(string $projectId, ?string $managementKey) { $this->httpClient = new Client(); + + if (!empty($_ENV['DESCOPE_LOG_PATH'])) { + $log = new Logger('descope_guzzle_log'); + $log->pushHandler(new StreamHandler($_ENV['DESCOPE_LOG_PATH'], Logger::DEBUG)); + $stack = HandlerStack::create(); + $stack->push( + Middleware::log( + $log, + new MessageFormatter(MessageFormatter::DEBUG) + ) + ); + $this->httpClient = new Client(['handler' => $stack]); + } else { + $this->httpClient = new Client(); + } + $this->projectId = $projectId; $this->managementKey = $managementKey ?? ''; } @@ -71,7 +88,7 @@ private function transformEmptyArraysToObjects($data) * @param array $body Request body. * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. - * @throws AuthException If the request fails. + * @throws AuthException|GuzzleException|JsonException If the request fails. */ public function doPost(string $uri, array $body, ?bool $useManagementKey = false, ?string $refreshToken = null): array { @@ -90,8 +107,8 @@ public function doPost(string $uri, array $body, ?bool $useManagementKey = false $response = $this->httpClient->post( $uri, [ - 'headers' => $this->getHeaders($authToken), - 'body' => $jsonBody, + 'headers' => $this->getHeaders($authToken), + 'body' => $jsonBody, ] ); @@ -99,8 +116,13 @@ public function doPost(string $uri, array $body, ?bool $useManagementKey = false if (!is_object($response) || !method_exists($response, 'getBody') || !method_exists($response, 'getHeader')) { throw new AuthException(500, 'internal error', 'Invalid response from API'); } - $resp = json_decode($response->getBody()->getContents(), true); - return $resp; + + // Read Body + $body = $response->getBody(); + $body->rewind(); + $contents = $body->getContents() ?? []; + + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -119,7 +141,7 @@ public function doPost(string $uri, array $body, ?bool $useManagementKey = false * @param string $uri URI endpoint. * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. - * @throws AuthException If the request fails. + * @throws AuthException|GuzzleException|JsonException If the request fails. */ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken = null): array { @@ -143,9 +165,13 @@ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken if (!is_object($response) || !method_exists($response, 'getBody') || !method_exists($response, 'getHeader')) { throw new AuthException(500, 'internal error', 'Invalid response from API'); } - $resp = json_decode($response->getBody()->getContents(), true); - - return $resp; + + // Read Body + $body = $response->getBody(); + $body->rewind(); + $contents = $body->getContents() ?? []; + + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -160,9 +186,9 @@ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken /** * Generates a JWT response array with the given parameters. * - * @param array $resp Response data. - * @param string|null $refreshToken Refresh token. - * @param string|null $audience Audience. + * @param array $responseBody + * @param string|null $refreshToken Refresh token. + * @param string|null $audience Audience. * @return array JWT response array. */ public function generateJwtResponse(array $responseBody, ?string $refreshToken = null, ?string $audience = null): array diff --git a/src/SDK/Auth/Password.php b/src/SDK/Auth/Password.php index 03ba143..1b5659f 100644 --- a/src/SDK/Auth/Password.php +++ b/src/SDK/Auth/Password.php @@ -10,6 +10,9 @@ class Password { + /** + * @var API The API object for making authenticated requests. + */ private $api; /** @@ -29,7 +32,7 @@ public function __construct(API $api) * @param string $password Password for the new user. * @param array|null $user Optional user details. * @return array JWT response array. - * @throws AuthException If login ID or password is empty. + * @throws AuthException */ public function signUp(string $loginId, string $password, ?array $user = null, ?array $loginOptions = null): array { @@ -54,7 +57,7 @@ public function signUp(string $loginId, string $password, ?array $user = null, ? * @param string $loginId Login ID of the user. * @param string $password Password of the user. * @return array JWT response array. - * @throws AuthException If login ID or password is empty. + * @throws AuthException */ public function signIn(string $loginId, string $password): array { @@ -78,7 +81,7 @@ public function signIn(string $loginId, string $password): array * @param string|null $redirectUrl Optional redirect URL. * @param array|null $templateOptions Optional template options. * @return array Response array. - * @throws AuthException If login ID is empty. + * @throws AuthException */ public function sendReset(string $loginId, ?string $redirectUrl = null, ?array $templateOptions = null): array { @@ -104,7 +107,7 @@ public function sendReset(string $loginId, ?string $redirectUrl = null, ?array $ * * @param string $loginId Login ID of the user. * @param string $newPassword New password for the user. - * @throws AuthException If login ID, new password, or refresh token is empty. + * @throws AuthException */ public function update(string $loginId, string $newPassword, string $refreshToken): void { @@ -131,7 +134,7 @@ public function update(string $loginId, string $newPassword, string $refreshToke * @param string $oldPassword Old password of the user. * @param string $newPassword New password for the user. * @return array JWT response array. - * @throws AuthException If login ID, old password, or new password is empty. + * @throws AuthException */ public function replace(string $loginId, string $oldPassword, string $newPassword): array { diff --git a/src/SDK/Auth/SSO.php b/src/SDK/Auth/SSO.php index 71d4b46..3f5bee6 100644 --- a/src/SDK/Auth/SSO.php +++ b/src/SDK/Auth/SSO.php @@ -10,6 +10,9 @@ class SSO { + /** + * @var API The API object for making authenticated requests. + */ private $api; /** @@ -25,15 +28,15 @@ public function __construct(API $api) /** * SSO sign-in request. * - * @param string|null $tenant Tenant identifier. - * @param string|null $redirectUrl URL to redirect after authentication. - * @param string|null $prompt Prompt parameter. - * @param bool $stepup Whether to perform step-up authentication. - * @param bool $mfa Whether to enforce MFA. - * @param array $customClaims Custom claims to include in the token. - * @param string|null $ssoAppId SSO application identifier. + * @param string|null $tenant Tenant identifier. + * @param string|null $redirectUrl URL to redirect after authentication. + * @param string|null $prompt Prompt parameter. + * @param bool $stepup Whether to perform step-up authentication. + * @param bool $mfa Whether to enforce MFA. + * @param array $customClaims Custom claims to include in the token. + * @param string|null $ssoAppId SSO application identifier. * @return array Response array. - * @throws AuthException If tenant or redirect URL validation fails. + * @throws AuthException */ public function signIn(?string $tenant = null, ?string $redirectUrl = null, ?string $prompt = null, bool $stepup = false, bool $mfa = false, array $customClaims = [], ?string $ssoAppId = null): array { @@ -58,7 +61,7 @@ public function signIn(?string $tenant = null, ?string $redirectUrl = null, ?str /** * Exchanges SSO code for authentication. * - * @param string|null $code The exchange code. + * @param string|null $code The exchange code. * @return array Response array. */ public function exchangeToken(?string $code = null): array @@ -74,9 +77,9 @@ public function exchangeToken(?string $code = null): array /** * Composes the SSO sign-in URL. * - * @param string|null $tenant Tenant identifier. - * @param string|null $redirectUrl Redirect URL. - * @param string|null $prompt Prompt parameter. + * @param string|null $tenant Tenant identifier. + * @param string|null $redirectUrl Redirect URL. + * @param string|null $prompt Prompt parameter. * @return string Composed URL. */ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string $prompt): string @@ -101,8 +104,8 @@ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string /** * Validates the tenant parameter. * - * @param string|null $tenant The tenant identifier. - * @throws AuthException If the tenant is invalid. + * @param string|null $tenant The tenant identifier. + * @throws AuthException */ private function validateTenant(?string $tenant): void { @@ -114,8 +117,8 @@ private function validateTenant(?string $tenant): void /** * Validates the redirect URL parameter. * - * @param string|null $redirectUrl The redirect URL. - * @throws AuthException If the redirect URL is invalid. + * @param string|null $redirectUrl The redirect URL. + * @throws AuthException */ private function validateRedirectUrl(?string $redirectUrl): void { diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index a16bc9f..e68b0b6 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -25,7 +25,7 @@ class DescopeSDK /** * Constructor for DescopeSDK class. * - * @param SDKConfig $config Base configuration options for the SDK. + * @param array $config Base configuration options for the SDK. */ public function __construct(array $config) { @@ -49,53 +49,97 @@ public function __construct(array $config) $this->sso = new SSO($this->api); } - /** + /** * Verify if the JWT is valid and not expired. + * + * @param string|null $sessionToken The session token to verify. + * @return bool Verification result. + * @throws AuthException */ - public function verify($sessionToken) + public function verify($sessionToken = null) { + $sessionToken = $sessionToken ?? $_COOKIE[SESSION_COOKIE] ?? null; + + if (!$sessionToken) { + throw new \InvalidArgumentException('Session token is required.'); + } + $verifier = new Verifier($this->config); return $verifier->verify($sessionToken); } /** - * Refresh session token with refresh token. + * Refresh session token using the refresh token. + * + * @param string|null $refreshToken The refresh token to use. + * @return array The new session information. + * @throws AuthException */ - public function refreshSession($refreshToken) + public function refreshSession($refreshToken = null) { + $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + + if (!$refreshToken) { + throw new \InvalidArgumentException('Refresh token is required.'); + } + $verifier = new Verifier($this->config); return $verifier->refreshSession($refreshToken); } /** - * Verify if the JWT is valid and not expired. + * Verify and refresh the session using session and refresh tokens. + * + * @param string|null $sessionToken The session token. + * @param string|null $refreshToken The refresh token. + * @return array The refreshed session information. + * @throws AuthException */ - public function verifyAndRefreshSession($sessionToken, $refreshToken) + public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null) { + $sessionToken = $sessionToken ?? $_COOKIE[SESSION_COOKIE] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + + if (!$sessionToken || !$refreshToken) { + throw new \InvalidArgumentException('Session token and refresh token are required.'); + } + $verifier = new Verifier($this->config); return $verifier->verifyAndRefreshSession($sessionToken, $refreshToken); } /** - * Returns the JWT claims, if the JWT is valid. + * Get the JWT claims if the token is valid. + * + * @param string|null $token The token to extract claims from. + * @return array The JWT claims. + * @throws AuthException */ - public function getClaims($token) + public function getClaims($token = null) { + $token = $token ?? $_COOKIE[SESSION_COOKIE] ?? null; + + if (!$token) { + throw new \InvalidArgumentException('Token is required.'); + } + $extractor = new Extractor($this->config); return $extractor->getClaims($token); } /** - * Returns the user details, using the refresh token. + * Retrieve user details using the refresh token. * - * @param string $refreshToken The refresh token of the user. - * @return void - * @throws AuthException if the logout operation fails. + * @param string|null $refreshToken The refresh token of the user. + * @return array The user details. + * @throws AuthException */ - public function getUserDetails(string $refreshToken) + public function getUserDetails(string $refreshToken = null) { - if (empty(EndpointsV1::$ME_PATH)) { - throw new \RuntimeException('ME_PATH is not initialized.'); + $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + + if (!$refreshToken) { + throw new \InvalidArgumentException('Refresh token is required.'); } return $this->api->doGet( @@ -106,16 +150,18 @@ public function getUserDetails(string $refreshToken) } /** - * Logout a user from all devices. + * Logout a user using the refresh token. * - * @param string $refreshToken The refresh token of the user. + * @param string|null $refreshToken The refresh token of the user. * @return void - * @throws AuthException if the logout operation fails. + * @throws AuthException */ - public function logout(string $refreshToken): void + public function logout(string $refreshToken = null): void { - if (empty(EndpointsV1::$LOGOUT_PATH)) { - throw new \RuntimeException('ME_PATH is not initialized.'); + $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + + if (!$refreshToken) { + throw new \InvalidArgumentException('Refresh token is required.'); } $this->api->doPost( @@ -127,14 +173,20 @@ public function logout(string $refreshToken): void } /** - * Logout a user from all devices. + * Logout a user from all devices using the refresh token. * - * @param string $refreshToken The refresh token of the user. + * @param string|null $refreshToken The refresh token of the user. * @return void - * @throws AuthException if the logout operation fails. + * @throws AuthException */ - public function logoutAll(string $refreshToken): void + public function logoutAll(string $refreshToken = null): void { + $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + + if (!$refreshToken) { + throw new \InvalidArgumentException('Refresh token is required.'); + } + $this->api->doPost( EndpointsV1::LOGOUT_ALL_PATH, [], @@ -145,6 +197,8 @@ public function logoutAll(string $refreshToken): void /** * Get the Password component. + * + * @return Password The Password instance. */ public function password(): Password { @@ -153,6 +207,8 @@ public function password(): Password /** * Get the SSO component. + * + * @return SSO The SSO instance. */ public function sso(): SSO { @@ -161,6 +217,8 @@ public function sso(): SSO /** * Get the Management component. + * + * @return Management The Management instance. */ public function management(): Management { diff --git a/src/SDK/EndpointsV1.php b/src/SDK/EndpointsV1.php index ea9d606..40441e0 100644 --- a/src/SDK/EndpointsV1.php +++ b/src/SDK/EndpointsV1.php @@ -9,11 +9,17 @@ const DEFAULT_DOMAIN = "descope.com"; const DEFAULT_TIMEOUT_SECONDS = 60; +const SESSION_COOKIE = "DS"; +const REFRESH_COOKIE = "DSR"; + const PHONE_REGEX = '/^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?){0,}((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$/'; class EndpointsV1 { - private static $baseUrl = DEFAULT_URL_PREFIX . '.' . DEFAULT_DOMAIN; + public static $baseUrl = DEFAULT_URL_PREFIX . '.' . DEFAULT_DOMAIN; + + public static $SESSION_COOKIE_NAME = SESSION_COOKIE; + public static $REFRESH_COOKIE_NAME = REFRESH_COOKIE; public static $REFRESH_TOKEN_PATH; public static $SELECT_TENANT_PATH; @@ -62,6 +68,13 @@ class EndpointsV1 public static $REPLACE_PASSWORD_PATH; public static $PASSWORD_POLICY_PATH; + /** + * Set the base URL for API endpoints using the project ID. + * The project ID is used to determine the correct region-specific base URL. + * + * @param string $projectId The project ID for the Descope project. + * @return void + */ public static function setBaseUrl(string $projectId): void { $region = self::extractRegionFromProjectId($projectId); @@ -75,6 +88,12 @@ public static function setBaseUrl(string $projectId): void self::updatePaths(); } + /** + * Extracts the region information from the given project ID. + * + * @param string $projectId The project ID for the Descope project. + * @return string|null Returns the region if extracted, otherwise null. + */ public static function extractRegionFromProjectId(string $projectId): ?string { if (strlen($projectId) >= 32) { @@ -84,6 +103,11 @@ public static function extractRegionFromProjectId(string $projectId): ?string return null; } + /** + * Updates the API endpoint paths to reflect the currently set base URL. + * + * @return void + */ public static function updatePaths(): void { self::$REFRESH_TOKEN_PATH = self::$baseUrl . "/v1/auth/refresh"; @@ -139,6 +163,13 @@ class EndpointsV2 { private static $baseUrl; + /** + * Set the base URL for API endpoints using the project ID. + * The project ID is used to determine the correct region-specific base URL. + * + * @param string $projectId The project ID for the Descope project. + * @return void + */ public static function setBaseUrl(string $projectId): void { $region = EndpointsV1::extractRegionFromProjectId($projectId); @@ -151,6 +182,11 @@ public static function setBaseUrl(string $projectId): void self::$baseUrl = "$urlPrefix." . DEFAULT_DOMAIN; } + /** + * Fetches the public JWK set path with the proper base url. + * + * @return string Returns the public key path of the JWK set. + */ public static function getPublicKeyPath(): string { return self::$baseUrl . "/v2/keys"; diff --git a/src/SDK/Management/AssociatedTenant.php b/src/SDK/Management/AssociatedTenant.php index 25786b1..800e96a 100644 --- a/src/SDK/Management/AssociatedTenant.php +++ b/src/SDK/Management/AssociatedTenant.php @@ -5,33 +5,46 @@ class AssociatedTenant { /** - * Represents a tenant association for a User or Access Key. The tenant will be used to determine permissions and roles for the entity. + * Represents a tenant association for a User or Access Key. + * The tenant will be used to determine permissions and roles for the entity. * - * @var string The Tenant ID + * @var string The Tenant ID. */ public $tenantId; /** - * Represents the role names for a user in the Tenant + * Represents the role names for a user in the Tenant. * - * @var array The Role Names + * @var array The Role Names. */ public $roleNames = []; /** - * Represents the role IDs for a user in the Tenant + * Represents the role IDs for a user in the Tenant. * - * @var array The Role IDs + * @var array The Role IDs. */ public $roleIds = []; - public function __construct($tenantId, $roleNames = [], $roleIds = []) + /** + * Constructor for the AssociatedTenant class. + * + * @param string $tenantId The Tenant ID. + * @param array $roleNames The role names for the user in the tenant. + * @param array $roleIds The role IDs for the user in the tenant. + */ + public function __construct(string $tenantId, array $roleNames = [], array $roleIds = []) { $this->tenantId = $tenantId; $this->roleNames = $roleNames; $this->roleIds = $roleIds; } + /** + * Converts the AssociatedTenant object to an associative array. + * + * @return array The associative array representation of the tenant and roles. + */ public function toArray(): array { return [ @@ -40,4 +53,4 @@ public function toArray(): array 'roleIds' => $this->roleIds, ]; } -} +} \ No newline at end of file diff --git a/src/SDK/Management/Management.php b/src/SDK/Management/Management.php index 070f454..77899b8 100644 --- a/src/SDK/Management/Management.php +++ b/src/SDK/Management/Management.php @@ -4,8 +4,17 @@ use Descope\SDK\API; +/** + * Class Management + * + * Represents the management functionality for Descope, providing access to + * user management capabilities. + */ class Management { + /** + * @var User The User management component. + */ public User $user; /** @@ -20,6 +29,8 @@ public function __construct(API $auth) /** * Get the User Management component. + * + * @return User The User management instance. */ public function user(): User { diff --git a/src/SDK/Management/MgmtV1.php b/src/SDK/Management/MgmtV1.php index 78f60f1..c4e4254 100644 --- a/src/SDK/Management/MgmtV1.php +++ b/src/SDK/Management/MgmtV1.php @@ -5,12 +5,20 @@ use Descope\SDK\EndpointsV1; + const DEFAULT_URL_PREFIX = "https://api"; const DEFAULT_DOMAIN = "descope.com"; class MgmtV1 { + /** + * Base URL for the Descope API. + * + * @var string + */ public static string $baseUrl = DEFAULT_URL_PREFIX . '.' . DEFAULT_DOMAIN; + + // Paths for various management operations public static string $TEMPLATE_EXPORT_PATH; public static string $TEMPLATE_IMPORT_PATH; public static string $FLOW_EXPORT_PATH; @@ -89,6 +97,12 @@ class MgmtV1 public static string $TENANT_UPDATE_PATH; public static string $TENANT_CREATE_PATH; + /** + * Sets the base URL based on the project ID, taking into account the region. + * + * @param string $projectId The project ID for determining the region. + * @return void + */ public static function setBaseUrl(string $projectId): void { $region = self::extractRegionFromProjectId($projectId); @@ -102,6 +116,12 @@ public static function setBaseUrl(string $projectId): void self::updatePaths(); } + /** + * Extracts the region from a given project ID. + * + * @param string $projectId The project ID to extract the region from. + * @return string|null The extracted region or null if not found. + */ private static function extractRegionFromProjectId(string $projectId): ?string { if (strlen($projectId) >= 32) { @@ -111,6 +131,11 @@ private static function extractRegionFromProjectId(string $projectId): ?string return null; } + /** + * Updates all API endpoint paths based on the current base URL. + * + * @return void + */ private static function updatePaths(): void { // Tenant @@ -210,13 +235,39 @@ private static function updatePaths(): void } } +/** + * Class representing login options for various authentication methods. + */ class LoginOptions { + /** + * @var bool Indicates if step-up authentication is required. + */ public bool $stepup; + + /** + * @var bool Indicates if MFA is required. + */ public bool $mfa; + + /** + * @var array|null Custom claims to include in the JWT. + */ public ?array $customClaims; + + /** + * @var array|null Options for templates. + */ public ?array $templateOptions; + /** + * Constructor for the LoginOptions class. + * + * @param bool $stepup Whether step-up is required. + * @param bool $mfa Whether MFA is required. + * @param array|null $customClaims Additional custom claims for the JWT. + * @param array|null $templateOptions Options for templates. + */ public function __construct( bool $stepup = false, bool $mfa = false, @@ -229,17 +280,25 @@ public function __construct( $this->templateOptions = $templateOptions; } - public function toArray() + /** + * Converts the LoginOptions object to an array. + * + * @return array The array representation of the login options. + */ + public function toArray(): array { return [ 'stepup' => $this->stepup, 'mfa' => $this->mfa, 'customClaims' => $this->customClaims, - 'templateOptions' => $this->templateOptions + 'templateOptions' => $this->templateOptions, ]; } } +/** + * Class representing different delivery methods for authentication. + */ class DeliveryMethod { public const WHATSAPP = 1; @@ -248,43 +307,42 @@ class DeliveryMethod public const EMBEDDED = 4; public const VOICE = 5; + /** + * @var int The delivery method value. + */ private int $value; + /** + * Constructor for the DeliveryMethod class. + * + * @param int $value The delivery method value. + */ private function __construct(int $value) { $this->value = $value; } - public static function WHATSAPP(): self - { - return new self(self::WHATSAPP); - } - - public static function SMS(): self - { - return new self(self::SMS); - } - - public static function EMAIL(): self - { - return new self(self::EMAIL); - } - - public static function EMBEDDED(): self - { - return new self(self::EMBEDDED); - } - - public static function VOICE(): self - { - return new self(self::VOICE); - } + public static function WHATSAPP(): self { return new self(self::WHATSAPP); } + public static function SMS(): self { return new self(self::SMS); } + public static function EMAIL(): self { return new self(self::EMAIL); } + public static function EMBEDDED(): self { return new self(self::EMBEDDED); } + public static function VOICE(): self { return new self(self::VOICE); } + /** + * Gets the value of the delivery method. + * + * @return int The delivery method value. + */ public function getValue(): int { return $this->value; } + /** + * Converts the delivery method to a string. + * + * @return string The string representation of the delivery method. + */ public function __toString(): string { return (string) $this->value; diff --git a/src/SDK/Management/User.php b/src/SDK/Management/User.php index dd7f4d6..5ad9a27 100644 --- a/src/SDK/Management/User.php +++ b/src/SDK/Management/User.php @@ -10,6 +10,7 @@ use Descope\SDK\Management\LoginOptions; use Descope\SDK\Management\UserPassword; use Descope\SDK\API; +use GuzzleHttp\Exception\RequestException; class UserObj { @@ -340,22 +341,23 @@ public function deleteAllTestUsers(): void ); } + /** + * @throws AuthException + */ public function load(string $loginId): array { - $response = $this->api->doGet( + return $this->api->doGet( MgmtV1::$USER_LOAD_PATH . "?loginId=" . $loginId, true ); - return $response; } public function loadByUserId(string $userId): array { - $response = $this->api->doGet( + return $this->api->doGet( MgmtV1::$USER_LOAD_PATH . "?userId=" . $userId, true ); - return $response; } /** @@ -425,7 +427,7 @@ public function searchAll( $allowedStatuses = ['enabled', 'disabled', 'invited']; if ($statuses !== null) { foreach ($statuses as $status) { - if (!in_array($status, $allowedStatuses)) { + if (!in_array($status, $allowedStatuses, true)) { throw new AuthException( 400, 'ERROR_TYPE_INVALID_ARGUMENT', @@ -462,13 +464,11 @@ public function searchAll( $jsonBody = json_encode($body); try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USERS_SEARCH_PATH, $body, true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -493,18 +493,18 @@ private function sortToArray(array $sort): array /** * Retrieve the provider token for a user. * - * @param string $loginId The login ID of the user. - * @param string $provider The name of the provider. + * @param string $loginId The login ID of the user. + * @param string $provider The name of the provider. * @return array The provider token details. + * @throws AuthException */ public function getProviderToken(string $loginId, string $provider): array { try { - $response = $this->api->doGet( + return $this->api->doGet( MgmtV1::$USER_GET_PROVIDER_TOKEN . "?loginId=" . $loginId . "&provider=" . $provider. "&withRefreshToken=true", true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -515,19 +515,18 @@ public function getProviderToken(string $loginId, string $provider): array /** * Activate a user. * - * @param string $loginId The login ID of the user. + * @param string $loginId The login ID of the user. * @return array The activation status. + * @throws AuthException */ public function activate(string $loginId): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_STATUS_PATH, ['loginId' => $loginId, 'status' => 'enabled'], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -538,18 +537,18 @@ public function activate(string $loginId): array /** * Deactivate a user. * - * @param string $loginId The login ID of the user. + * @param string $loginId The login ID of the user. * @return array The deactivation status. + * @throws AuthException */ public function deactivate(string $loginId): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_STATUS_PATH, ['loginId' => $loginId, 'status' => 'disabled'], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -560,19 +559,19 @@ public function deactivate(string $loginId): array /** * Update the login ID of a user. * - * @param string $loginId The current login ID of the user. - * @param string $newLoginId The new login ID for the user. + * @param string $loginId The current login ID of the user. + * @param string $newLoginId The new login ID for the user. * @return array The updated user details. + * @throws AuthException */ public function updateLoginId(string $loginId, string $newLoginId): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_LOGIN_ID_PATH, ['loginId' => $loginId, 'newLoginId' => $newLoginId], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -583,21 +582,20 @@ public function updateLoginId(string $loginId, string $newLoginId): array /** * Update the email address of a user. * - * @param string $loginId The login ID of the user. - * @param string $email The new email address. - * @param bool $verified Whether the email is verified. + * @param string $loginId The login ID of the user. + * @param string $email The new email address. + * @param bool $verified Whether the email is verified. * @return array The updated user details. + * @throws AuthException */ public function updateEmail(string $loginId, string $email, bool $verified): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_EMAIL_PATH, ['loginId' => $loginId, 'email' => $email, 'verified' => $verified], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -608,21 +606,20 @@ public function updateEmail(string $loginId, string $email, bool $verified): arr /** * Update the phone number of a user. * - * @param string $loginId The login ID of the user. - * @param string $phone The new phone number. - * @param bool $verified Whether the phone number is verified. + * @param string $loginId The login ID of the user. + * @param string $phone The new phone number. + * @param bool $verified Whether the phone number is verified. * @return array The updated user details. + * @throws AuthException */ public function updatePhone(string $loginId, string $phone, bool $verified): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_PHONE_PATH, ['loginId' => $loginId, 'phone' => $phone, 'verified' => $verified], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -633,12 +630,13 @@ public function updatePhone(string $loginId, string $phone, bool $verified): arr /** * Update the display name of a user. * - * @param string $loginId The login ID of the user. - * @param string $displayName The new display name. - * @param string|null $givenName The given name (optional). - * @param string|null $middleName The middle name (optional). - * @param string|null $familyName The family name (optional). + * @param string $loginId The login ID of the user. + * @param string $displayName The new display name. + * @param string|null $givenName The given name (optional). + * @param string|null $middleName The middle name (optional). + * @param string|null $familyName The family name (optional). * @return array The updated user details. + * @throws AuthException */ public function updateDisplayName( string $loginId, @@ -659,13 +657,11 @@ public function updateDisplayName( } try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_NAME_PATH, $body, true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -676,20 +672,19 @@ public function updateDisplayName( /** * Update the profile picture of a user. * - * @param string $loginId The login ID of the user. - * @param string $picture The new profile picture URL. + * @param string $loginId The login ID of the user. + * @param string $picture The new profile picture URL. * @return array The updated user details. + * @throws AuthException */ public function updatePicture(string $loginId, string $picture): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_PICTURE_PATH, ['loginId' => $loginId, 'picture' => $picture], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -700,21 +695,20 @@ public function updatePicture(string $loginId, string $picture): array /** * Update a custom attribute for a user. * - * @param string $loginId The login ID of the user. - * @param string $attributeKey The key of the custom attribute. - * @param mixed $attributeValue The value of the custom attribute. + * @param string $loginId The login ID of the user. + * @param string $attributeKey The key of the custom attribute. + * @param mixed $attributeValue The value of the custom attribute. * @return array The updated user details. + * @throws AuthException */ public function updateCustomAttribute(string $loginId, string $attributeKey, $attributeValue): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_UPDATE_CUSTOM_ATTRIBUTE_PATH, ['loginId' => $loginId, 'attributeKey' => $attributeKey, 'attributeValue' => $attributeValue], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -725,20 +719,19 @@ public function updateCustomAttribute(string $loginId, string $attributeKey, $at /** * Set roles for a user. * - * @param string $loginId The login ID of the user. - * @param array $roleNames The list of role names to set. + * @param string $loginId The login ID of the user. + * @param array $roleNames The list of role names to set. * @return array The updated user details. + * @throws AuthException */ public function setRoles(string $loginId, array $roleNames): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_SET_ROLE_PATH, ['loginId' => $loginId, 'roleNames' => $roleNames], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -749,20 +742,19 @@ public function setRoles(string $loginId, array $roleNames): array /** * Add roles to a user. * - * @param string $loginId The login ID of the user. - * @param array $roleNames The list of role names to add. + * @param string $loginId The login ID of the user. + * @param array $roleNames The list of role names to add. * @return array The updated user details. + * @throws AuthException */ public function addRoles(string $loginId, array $roleNames): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_ADD_ROLE_PATH, ['loginId' => $loginId, 'roleNames' => $roleNames], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -773,20 +765,19 @@ public function addRoles(string $loginId, array $roleNames): array /** * Remove roles from a user. * - * @param string $loginId The login ID of the user. - * @param array $roleNames The list of role names to remove. + * @param string $loginId The login ID of the user. + * @param array $roleNames The list of role names to remove. * @return array The updated user details. + * @throws AuthException */ public function removeRoles(string $loginId, array $roleNames): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_REMOVE_ROLE_PATH, ['loginId' => $loginId, 'roleNames' => $roleNames], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -797,20 +788,19 @@ public function removeRoles(string $loginId, array $roleNames): array /** * Set SSO applications for a user. * - * @param string $loginId The login ID of the user. - * @param array $ssoAppIds The list of SSO application IDs to set. + * @param string $loginId The login ID of the user. + * @param array $ssoAppIds The list of SSO application IDs to set. * @return array The updated user details. + * @throws AuthException */ public function setSsoApps(string $loginId, array $ssoAppIds): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_SET_SSO_APPS, ['loginId' => $loginId, 'ssoAppIds' => $ssoAppIds], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -821,20 +811,19 @@ public function setSsoApps(string $loginId, array $ssoAppIds): array /** * Add SSO applications to a user. * - * @param string $loginId The login ID of the user. - * @param array $ssoAppIds The list of SSO application IDs to add. + * @param string $loginId The login ID of the user. + * @param array $ssoAppIds The list of SSO application IDs to add. * @return array The updated user details. + * @throws AuthException */ public function addSsoApps(string $loginId, array $ssoAppIds): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_ADD_SSO_APPS, ['loginId' => $loginId, 'ssoAppIds' => $ssoAppIds], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -845,20 +834,19 @@ public function addSsoApps(string $loginId, array $ssoAppIds): array /** * Remove SSO applications from a user. * - * @param string $loginId The login ID of the user. - * @param array $ssoAppIds The list of SSO application IDs to remove. + * @param string $loginId The login ID of the user. + * @param array $ssoAppIds The list of SSO application IDs to remove. * @return array The updated user details. + * @throws AuthException */ public function removeSsoApps(string $loginId, array $ssoAppIds): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_REMOVE_SSO_APPS, ['loginId' => $loginId, 'ssoAppIds' => $ssoAppIds], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -869,20 +857,19 @@ public function removeSsoApps(string $loginId, array $ssoAppIds): array /** * Add a tenant to a user. * - * @param string $loginId The login ID of the user. - * @param string $tenantId The tenant ID to add. + * @param string $loginId The login ID of the user. + * @param string $tenantId The tenant ID to add. * @return array The updated user details. + * @throws AuthException */ public function addTenant(string $loginId, string $tenantId): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_ADD_TENANT_PATH, ['loginId' => $loginId, 'tenantId' => $tenantId], true ); - - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -893,19 +880,19 @@ public function addTenant(string $loginId, string $tenantId): array /** * Remove a tenant from a user. * - * @param string $loginId The login ID of the user. - * @param string $tenantId The tenant ID to remove. + * @param string $loginId The login ID of the user. + * @param string $tenantId The tenant ID to remove. * @return array The updated user details. + * @throws AuthException */ public function removeTenant(string $loginId, string $tenantId): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_REMOVE_TENANT_PATH, ['loginId' => $loginId, 'tenantId' => $tenantId], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -916,20 +903,20 @@ public function removeTenant(string $loginId, string $tenantId): array /** * Set roles for a user in a tenant. * - * @param string $loginId The login ID of the user. - * @param string $tenantId The tenant ID. - * @param array $roleNames The list of role names to set. + * @param string $loginId The login ID of the user. + * @param string $tenantId The tenant ID. + * @param array $roleNames The list of role names to set. * @return array The updated user details. + * @throws AuthException */ public function setTenantRoles(string $loginId, string $tenantId, array $roleNames): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_SET_ROLE_PATH, ['loginId' => $loginId, 'tenantId' => $tenantId, 'roleNames' => $roleNames], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -940,20 +927,20 @@ public function setTenantRoles(string $loginId, string $tenantId, array $roleNam /** * Remove roles from a user in a tenant. * - * @param string $loginId The login ID of the user. - * @param string $tenantId The tenant ID. - * @param array $roleNames The list of role names to remove. + * @param string $loginId The login ID of the user. + * @param string $tenantId The tenant ID. + * @param array $roleNames The list of role names to remove. * @return array The updated user details. + * @throws AuthException */ public function removeTenantRoles(string $loginId, string $tenantId, array $roleNames): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_REMOVE_ROLE_PATH, ['loginId' => $loginId, 'tenantId' => $tenantId, 'roleNames' => $roleNames], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -964,9 +951,10 @@ public function removeTenantRoles(string $loginId, string $tenantId, array $role /** * Set a temporary password for a user. * - * @param string $loginId The login ID of the user. - * @param string $password The new temporary password. + * @param string $loginId The login ID of the user. + * @param UserPassword $password The new temporary password. * @return void + * @throws AuthException */ public function setTemporaryPassword(string $loginId, UserPassword $password): void { @@ -986,9 +974,10 @@ public function setTemporaryPassword(string $loginId, UserPassword $password): v /** * Set an active password for a user. * - * @param string $loginId The login ID of the user. - * @param string $password The new active password. + * @param string $loginId The login ID of the user. + * @param UserPassword $password The new active password. * @return void + * @throws AuthException */ public function setActivePassword(string $loginId, UserPassword $password): void { @@ -1008,10 +997,11 @@ public function setActivePassword(string $loginId, UserPassword $password): void /** * Set a password for a user. * - * @param string $loginId The login ID of the user. - * @param string $password The new password. - * @param bool $setActive Whether to set the password as active. + * @param string $loginId The login ID of the user. + * @param string $password The new password. + * @param bool $setActive Whether to set the password as active. * @return void + * @throws AuthException */ public function setPassword(string $loginId, string $password, bool $setActive = false): void { @@ -1031,10 +1021,11 @@ public function setPassword(string $loginId, string $password, bool $setActive = /** * Update the user's password. * - * @param string $loginId The login ID of the user. - * @param string $password The new password. - * @param bool $setActive Whether to set the user as active. + * @param string $loginId The login ID of the user. + * @param string $password The new password. + * @param bool $setActive Whether to set the user as active. * @return void + * @throws AuthException */ public function updatePassword(string $loginId, string $password, bool $setActive): void { @@ -1054,8 +1045,9 @@ public function updatePassword(string $loginId, string $password, bool $setActiv /** * Expire the user's password. * - * @param string $loginId The login ID of the user. + * @param string $loginId The login ID of the user. * @return void + * @throws AuthException */ public function expirePassword(string $loginId): void { @@ -1075,8 +1067,9 @@ public function expirePassword(string $loginId): void /** * Remove all passkeys for a user. * - * @param string $loginId The login ID of the user. + * @param string $loginId The login ID of the user. * @return void + * @throws AuthException */ public function removeAllPasskeys(string $loginId): void { @@ -1096,10 +1089,11 @@ public function removeAllPasskeys(string $loginId): void /** * Generate an OTP for a test user. * - * @param string $loginId The login ID of the user. - * @param string $method The delivery method for the OTP. - * @param array|null $loginOptions Optional login options. + * @param string $loginId The login ID of the user. + * @param int $method The delivery method for the OTP. + * @param LoginOptions|null $loginOptions Optional login options. * @return array The generated OTP details. + * @throws AuthException */ public function generateOtpForTestUser(string $loginId, int $method, ?LoginOptions $loginOptions = null): array { @@ -1124,16 +1118,17 @@ public function generateOtpForTestUser(string $loginId, int $method, ?LoginOptio /** * Generate a magic link for a test user. * - * @param string $loginId The login ID of the user. - * @param string $method The delivery method for the magic link. - * @param string $uri The URI for the magic link. - * @param array|null $loginOptions Optional login options. + * @param string $loginId The login ID of the user. + * @param int $method The delivery method for the magic link. + * @param string $uri The URI for the magic link. + * @param LoginOptions|null $loginOptions Optional login options. * @return array The generated magic link details. + * @throws AuthException */ public function generateMagicLinkForTestUser(string $loginId, int $method, string $uri, ?LoginOptions $loginOptions = null): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_GENERATE_MAGIC_LINK_FOR_TEST_PATH, [ 'loginId' => $loginId, @@ -1143,7 +1138,6 @@ public function generateMagicLinkForTestUser(string $loginId, int $method, strin ], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -1155,16 +1149,16 @@ public function generateMagicLinkForTestUser(string $loginId, int $method, strin * Generate Enchanted Link for the given login ID of a test user. * This is useful when running tests and don't want to use 3rd party messaging services. * - * @param string $loginId The login ID of the test user being validated. - * @param string $uri Optional redirect uri which will be used instead of any global configuration. - * @param array|null $loginOptions Optional, can be provided to set custom claims to the generated jwt. + * @param string $loginId The login ID of the test user being validated. + * @param string $uri The redirect URI to be used instead of any global configuration. + * @param array|null $loginOptions Optional, can be provided to set custom claims to the generated jwt. * @return array The enchanted link for the login (exactly as it sent via Email or Phone messaging) and pendingRef. - * @throws AuthException if the operation fails. + * @throws AuthException */ public function generateEnchantedLinkForTestUser(string $loginId, string $uri, ?LoginOptions $loginOptions = null): array { try { - $response = $this->api->doPost( + return $this->api->doPost( MgmtV1::$USER_GENERATE_ENCHANTED_LINK_FOR_TEST_PATH, [ 'loginId' => $loginId, @@ -1173,7 +1167,6 @@ public function generateEnchantedLinkForTestUser(string $loginId, string $uri, ? ], true ); - return $response; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; @@ -1185,17 +1178,20 @@ public function generateEnchantedLinkForTestUser(string $loginId, string $uri, ? * Generate Embedded Link for the given user login ID. * The return value is a token that can be verified via magic link, or using flows. * - * @param string $loginId The login ID of the user to authenticate with. - * @param array|null $customClaims Additional claims to place on the jwt after verification. + * @param string $loginId The login ID of the user to authenticate with. + * @param array|null $customClaims Additional claims to place on the jwt after verification. * @return string The token to be used in the verification process. - * @throws AuthException if the operation fails. + * @throws AuthException */ public function generateEmbeddedLink(string $loginId, ?array $customClaims = null): string { try { $response = $this->api->doPost( MgmtV1::$USER_GENERATE_EMBEDDED_LINK_PATH, - ['loginId' => $loginId, 'customClaims' => $customClaims], + [ + 'loginId' => $loginId, + 'customClaims' => $customClaims + ], true ); @@ -1207,28 +1203,36 @@ public function generateEmbeddedLink(string $loginId, ?array $customClaims = nul } } - // /** - // * Retrieve users' authentication history, by the given user's IDs. - // * - // * @param array $userIds List of users' IDs. - // * @return array The authentication history of the users. - // * @throws AuthException if the operation fails. - // */ - // public function history(array $userIds): array { - // try { - // $response = $this->api->doPost( - // MgmtV1::$USER_HISTORY_PATH, - // $userIds, - // true - // ); + /** + * Retrieve users' authentication history, by the given user's IDs. + * + * @param array $userIds List of users' IDs. + * @return array The authentication history of the users. + * @throws AuthException + */ + public function history(array $userIds): array { + try { + $response = $this->api->doPost( + MgmtV1::$USER_HISTORY_PATH, + ['userIds' => $userIds], + true + ); - // return $response; - // } catch (RequestException $e) { - // $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; - // $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; - // throw new AuthException($statusCode, 'RequestException', $e->getMessage()); - // } - // } + // Process response to ensure it's an array of structured UserHistory objects + return array_map(function($historyItem) { + return [ + 'userId' => $historyItem['userId'] ?? '', + 'loginTime' => $historyItem['loginTime'] ?? 0, + 'city' => $historyItem['city'] ?? '', + 'country' => $historyItem['country'] ?? '', + 'ip' => $historyItem['ip'] ?? '', + ]; + }, $response['usersAuthHistory'] ?? []); + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } + } public function composeCreateBody( string $loginId, @@ -1253,39 +1257,39 @@ public function composeCreateBody( ?array $ssoAppIds, ?UserPassword $password ): array { - $res = [ - 'loginId' => $loginId, - 'email' => $email, - 'phone' => $phone, - 'displayName' => $displayName, - 'givenName' => $givenName, - 'middleName' => $middleName, - 'familyName' => $familyName, - 'roleNames' => $roleNames, - 'userTenants' => $userTenants, - 'invited' => $invited, - 'test' => $test, - 'picture' => $picture, + $res = array_filter([ + 'loginId' => $loginId ?? null, + 'email' => $email ?? null, + 'phone' => $phone ?? null, + 'displayName' => $displayName ?? null, + 'givenName' => $givenName ?? null, + 'middleName' => $middleName ?? null, + 'familyName' => $familyName ?? null, + 'roleNames' => $roleNames ?? null, + 'userTenants' => $userTenants ?? null, + 'invite' => $invited ?? null, + 'test' => $test ?? null, + 'picture' => $picture ?? null, 'customAttributes' => $customAttributes ?? (object)[], - 'verifiedEmail' => $verifiedEmail, - 'verifiedPhone' => $verifiedPhone, - 'inviteUrl' => $inviteUrl, - 'sendMail' => $sendMail, - 'sendSms' => $sendSms, - 'additionalLoginIds' => $additionalLoginIds, - 'ssoAppIds' => $ssoAppIds, - ]; + 'verifiedEmail' => $verifiedEmail ?? null, + 'verifiedPhone' => $verifiedPhone ?? null, + 'inviteUrl' => $inviteUrl ?? null, + 'sendMail' => $sendMail ?? null, + 'sendSMS' => $sendSms ?? null, + 'additionalLoginIds' => $additionalLoginIds ?? null, + 'ssoAppIds' => $ssoAppIds ?? null, + ], static function ($value) { + return !empty($value); + }); if ($password !== null) { if (isset($password->cleartext)) { $res['password'] = $password->cleartext; - } else { - if (isset($password->hashedPassword)) { - $res['hashedPassword'] = $password->hashedPassword; - } + } else if (isset($password->hashed)) { + $res['hashedPassword'] = $password->hashed; } } - + return $res; } @@ -1379,13 +1383,11 @@ public function composeUpdateBody( if ($password !== null) { if (isset($password->cleartext)) { $res['password'] = $password->cleartext; - } else { - if (isset($password->hashedPassword)) { - $res['hashedPassword'] = $password->hashedPassword; - } - } + } else if (isset($password->hashed)) { + $res['hashedPassword'] = $password->hashed; } return $res; } + } } diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index ddc9e31..9601ffb 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -13,18 +13,19 @@ use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\UserObj; use Descope\SDK\Auth\LoginOptions; -use Descope\SDK\DeliveryMethod; +use Descope\SDK\Common\DeliveryMethod; +use Descope\SDK\Exception\AuthException; +use GuzzleHttp\Exception\RequestException; class UserPasswordTest extends TestCase { - private $descopeSDK; + private DescopeSDK $descopeSDK; protected function setUp(): void { - // Assuming $config is an array with necessary configuration $config = [ - 'projectId' => 'YOUR_PROJECT_ID', - 'managementKey' => 'YOUR_MANAGEMENT_KEY', + 'projectId' => 'P2OkfVnJi5Ht7mpCqHjx17nV5epH', + 'managementKey' => 'K2nmY9zuewgPZpRpTAmGW6hlBYYp6EiZfXeMkJyHDg89NOpobIlMbSZzb388BiilhPfg04l', ]; $this->descopeSDK = new DescopeSDK($config); @@ -47,10 +48,11 @@ public function testCreateUser() "http://example.com/invite", ["additionalLoginId1"], ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"], - new UserPassword(cleartext: "password123"), - [], + new UserPassword("password123"), + ["user"], [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant Admin"])] ); + $this->assertArrayHasKey('userId', $response); print_r($response); } @@ -75,6 +77,7 @@ public function testCreateTestUser() ["user"], [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant User"])] ); + $this->assertArrayHasKey('userId', $response); print_r($response); } @@ -96,10 +99,12 @@ public function testInviteUser() true, true, ["additionalLoginId3"], - [], + ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"], new UserPassword(hashed: new UserPasswordBcrypt("$2y$10$/brZw23J/ya5sOJl8vm7H.BqhDnLqH4ohtSKcZYvSVP/hE6veK.0K")), + ["user"], [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant User"])] ); + $this->assertArrayHasKey('userId', $response); print_r($response); } @@ -143,7 +148,9 @@ public function testInviteBatchUsers() new UserPassword(cleartext: "password456") ) ]; + $response = $this->descopeSDK->management->user->inviteBatch($users, "http://example.com/invitebatch", true, true); + $this->assertArrayHasKey('userIds', $response); print_r($response); } @@ -160,37 +167,57 @@ public function testUpdateUser() "http://example.com/newpicture.jpg", ["dob" => "newvalue1"], true, - true + true, + ["additionalLoginId1"], + ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"] ); + $this->assertTrue(true); // Assert no exception thrown } public function testDeleteUser() { $this->descopeSDK->management->user->delete("testuser1"); + $this->assertTrue(true); // Assert no exception thrown } public function testLoadUser() { - $response = $this->descopeSDK->management->user->load("gaokevin1"); + $response = $this->descopeSDK->management->user->load("testuser1"); + $this->assertArrayHasKey('userId', $response); print_r($response); } public function testLoadUserByUserId() { $response = $this->descopeSDK->management->user->loadByUserId("U2goH2ldn4SzXoFm6IWKlRiEq6JV"); + $this->assertArrayHasKey('userId', $response); print_r($response); } - public function testSignIn() + public function testGenerateEmbeddedLink() { - $response = $this->descopeSDK->password->signIn("gaokevin", "6ny8UPNgTVtwB,tcjltg"); + $response = $this->descopeSDK->management->user->generateEmbeddedLink("testuser1"); + $this->assertIsString($response); + print_r($response); + } + + public function testGenerateEnchantedLinkForTestUser() + { + $loginOptions = new LoginOptions(['stepup' => true, 'mfa' => true]); + $response = $this->descopeSDK->management->user->generateEnchantedLinkForTestUser( + "testuser1", + "http://example.com/redirect", + $loginOptions + ); + $this->assertArrayHasKey('link', $response); + $this->assertArrayHasKey('pendingRef', $response); print_r($response); } public function testLogoutUser() { - $response = $this->descopeSDK->password->signIn("gaokevin", "6ny8UPNgTVtwB,tcjltg"); - $this->descopeSDK->logout($response['refreshSessionToken']); + $response = $this->descopeSDK->management->user->logout("testuser1"); + $this->assertTrue(true); // Assert no exception thrown } public function testSearchAllUsers() @@ -199,7 +226,7 @@ public function testSearchAllUsers() [], [], 10, - 1, + 0, false, false, [], @@ -208,109 +235,30 @@ public function testSearchAllUsers() ["+14152464801"], [] ); + $this->assertArrayHasKey('users', $response); print_r($response); } public function testGetProviderToken() { - $response = $this->descopeSDK->management->user->getProviderToken("gaokevin1", "google"); + $response = $this->descopeSDK->management->user->getProviderToken("testuser1", "google"); + $this->assertArrayHasKey('accessToken', $response); print_r($response); } public function testActivateUser() { - $response = $this->descopeSDK->management->user->activate("gaokevin1"); + $response = $this->descopeSDK->management->user->activate("testuser1"); + $this->assertArrayHasKey('status', $response); print_r($response); } public function testDeactivateUser() { $response = $this->descopeSDK->management->user->deactivate("testuser1"); + $this->assertArrayHasKey('status', $response); print_r($response); } +} - public function testUpdateLoginId() - { - $response = $this->descopeSDK->management->user->updateLoginId("testuser1", "newtestuser1"); - print_r($response); - } - - public function testUpdateEmail() - { - $response = $this->descopeSDK->management->user->updateEmail("testuser1", "newtestuser1@example.com", true); - print_r($response); - } - - public function testUpdatePhone() - { - $response = $this->descopeSDK->management->user->updatePhone("testuser1", "+14152464801", true); - print_r($response); - } - - public function testUpdateDisplayName() - { - $response = $this->descopeSDK->management->user->updateDisplayName("testuser1", "Updated Display Name", "Updated Given Name", "Updated Middle Name", "Updated Family Name"); - print_r($response); - } - - public function testUpdatePicture() - { - $response = $this->descopeSDK->management->user->updatePicture("testuser1", "http://example.com/newpicture.jpg"); - print_r($response); - } - - public function testUpdateCustomAttribute() - { - $response = $this->descopeSDK->management->user->updateCustomAttribute("testuser1", "customAttr1", "newvalue1"); - print_r($response); - } - - public function testSetRoles() - { - $response = $this->descopeSDK->management->user->setRoles("testuser1", ["user"]); - print_r($response); - } - - public function testAddRoles() - { - $response = $this->descopeSDK->management->user->addRoles("testuser1", ["admin"]); - print_r($response); - } - - public function testRemoveRoles() - { - $response = $this->descopeSDK->management->user->removeRoles("testuser1", ["admin"]); - print_r($response); - } - - public function testSetTenants() - { - $tenants = [ - new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant Admin"]), - new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBB", ["Tenant User"]) - ]; - $response = $this->descopeSDK->management->user->setTenants("testuser1", $tenants); - print_r($response); - } - - public function testAddTenants() - { - $tenants = [ - new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBC", ["Tenant Viewer"]) - ]; - $response = $this->descopeSDK->management->user->addTenants("testuser1", $tenants); - print_r($response); - } - - public function testRemoveTenants() - { - $response = $this->descopeSDK->management->user->removeTenants("testuser1", ["T2SrweL5J2y8YOh8DyDbGpZXejBB"]); - print_r($response); - } - public function testUpdateDisplay() - { - $response = $this->descopeSDK->management->user->updateDisplay("testuser1", "Updated Display"); - print_r($response); - } -} From 2a27dec5bcc37d640aa7adc58e34e3de410ffe1c Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 18:04:13 -0700 Subject: [PATCH 05/13] simplified functions --- src/SDK/Token/Extractor.php | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index 603d2fe..fcee856 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -37,12 +37,7 @@ public function __construct($config) public function getClaims($sessionToken): array { $jws = $this->parseToken($sessionToken); - $claims = json_decode($jws->getPayload(), true); - - if (!is_array($claims)) { - return []; - } - return $claims; + return json_decode($jws->getPayload(), true) ?? []; } /** @@ -53,20 +48,14 @@ public function getClaims($sessionToken): array public function getUserDetails($refreshToken) { $client = $this->config->client; + $url = EndpointsV1::$ME_PATH; + $header = 'Bearer ' . $this->config->projectId . ":" . $refreshToken; + try { - $url = EndpointsV1::$ME_PATH; - $header = 'Bearer ' . $this->config->projectId . ":" . $refreshToken; - $res = $client->request( - 'GET', - $url, - [ - 'headers' => ['Authorization' => $header] - ] - ); - $jwkSets = json_decode($res->getBody(), true); - return $jwkSets; - } catch (RequestException $re) { - return $re->getMessage(); + $response = $client->get($url, ['headers' => ['Authorization' => $header]]); + return json_decode($response->getBody(), true); + } catch (RequestException $e) { + throw new TokenException('Failed to retrieve user details: ' . $e->getMessage()); } } From 556758517732616331d109ebbd9ba1214cbb4df3 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 18:06:27 -0700 Subject: [PATCH 06/13] fixed function defintions --- src/SDK/API.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index ae56c03..0715336 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -88,7 +88,7 @@ private function transformEmptyArraysToObjects($data) * @param array $body Request body. * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. - * @throws AuthException|GuzzleException|JsonException If the request fails. + * @throws AuthException|GuzzleException|\JsonException If the request fails. */ public function doPost(string $uri, array $body, ?bool $useManagementKey = false, ?string $refreshToken = null): array { @@ -141,7 +141,7 @@ public function doPost(string $uri, array $body, ?bool $useManagementKey = false * @param string $uri URI endpoint. * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. - * @throws AuthException|GuzzleException|JsonException If the request fails. + * @throws AuthException|GuzzleException|\JsonException If the request fails. */ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken = null): array { From 1806bdf5aede14cfa1b49055c92c4963c2bb6dfc Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 18:13:08 -0700 Subject: [PATCH 07/13] added files --- src/SDK/API.php | 40 ++++++++++++++++------------------------ src/SDK/DescopeSDK.php | 16 ++++++++-------- src/SDK/EndpointsV1.php | 10 ++++++++++ 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index 0715336..5a93b55 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -17,14 +17,6 @@ class API private $projectId; private $managementKey; - const SESSION_TOKEN_NAME = 'sessionToken'; - const REFRESH_SESSION_TOKEN_NAME = 'refreshSessionToken'; - const COOKIE_DATA_NAME = 'cookieData'; - - const SESSION_COOKIE_NAME = "DS"; - const REFRESH_SESSION_COOKIE_NAME = "DSR"; - const REDIRECT_LOCATION_COOKIE_NAME = "Location"; - /** * Constructor for API class. * @@ -244,21 +236,21 @@ private function generateAuthInfo(array $responseBody, ?string $refreshToken, bo $stJwt = $responseBody['sessionJwt'] ?? ''; if ($stJwt) { - $jwtResponse[self::SESSION_TOKEN_NAME] = $stJwt; + $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME] = $stJwt; } $rtJwt = $responseBody['refreshJwt'] ?? ''; if ($refreshToken) { - $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME] = $refreshToken; + $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME] = $refreshToken; } elseif ($rtJwt) { - $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME] = $rtJwt; + $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME] = $rtJwt; } $jwtResponse = $this->adjustProperties($jwtResponse, $userJwt); if ($userJwt) { - $jwtResponse[self::COOKIE_DATA_NAME] = [ + $jwtResponse[EndpointsV1::COOKIE_DATA_NAME] = [ 'exp' => $responseBody['cookieExpiration'] ?? 0, 'maxAge' => $responseBody['cookieMaxAge'] ?? 0, 'domain' => $responseBody['cookieDomain'] ?? '', @@ -271,29 +263,29 @@ private function generateAuthInfo(array $responseBody, ?string $refreshToken, bo private function adjustProperties(array $jwtResponse, bool $userJwt): array { - if (isset($jwtResponse[self::SESSION_TOKEN_NAME])) { - $jwtResponse['permissions'] = $jwtResponse[self::SESSION_TOKEN_NAME]['permissions'] ?? []; - $jwtResponse['roles'] = $jwtResponse[self::SESSION_TOKEN_NAME]['roles'] ?? []; - $jwtResponse['tenants'] = $jwtResponse[self::SESSION_TOKEN_NAME]['tenants'] ?? []; - } elseif (isset($jwtResponse[self::REFRESH_SESSION_TOKEN_NAME])) { - $jwtResponse['permissions'] = $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME]['permissions'] ?? []; - $jwtResponse['roles'] = $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME]['roles'] ?? []; - $jwtResponse['tenants'] = $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME]['tenants'] ?? []; + if (isset($jwtResponse[EndpointsV1::SESSION_TOKEN_NAME])) { + $jwtResponse['permissions'] = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['permissions'] ?? []; + $jwtResponse['roles'] = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['roles'] ?? []; + $jwtResponse['tenants'] = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['tenants'] ?? []; + } elseif (isset($jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME])) { + $jwtResponse['permissions'] = $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['permissions'] ?? []; + $jwtResponse['roles'] = $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['roles'] ?? []; + $jwtResponse['tenants'] = $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['tenants'] ?? []; } else { $jwtResponse['permissions'] = $jwtResponse['permissions'] ?? []; $jwtResponse['roles'] = $jwtResponse['roles'] ?? []; $jwtResponse['tenants'] = $jwtResponse['tenants'] ?? []; } - $issuer = $jwtResponse[self::SESSION_TOKEN_NAME]['iss'] ?? - $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME]['iss'] ?? + $issuer = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['iss'] ?? + $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['iss'] ?? $jwtResponse['iss'] ?? ''; $issuerParts = explode("/", $issuer); $jwtResponse['projectId'] = end($issuerParts); - $sub = $jwtResponse[self::SESSION_TOKEN_NAME]['sub'] ?? - $jwtResponse[self::REFRESH_SESSION_TOKEN_NAME]['sub'] ?? + $sub = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['sub'] ?? + $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['sub'] ?? $jwtResponse['sub'] ?? ''; if ($userJwt) { diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index e68b0b6..de9020a 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -58,7 +58,7 @@ public function __construct(array $config) */ public function verify($sessionToken = null) { - $sessionToken = $sessionToken ?? $_COOKIE[SESSION_COOKIE] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME_NAME] ?? null; if (!$sessionToken) { throw new \InvalidArgumentException('Session token is required.'); @@ -77,7 +77,7 @@ public function verify($sessionToken = null) */ public function refreshSession($refreshToken = null) { - $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { throw new \InvalidArgumentException('Refresh token is required.'); @@ -97,8 +97,8 @@ public function refreshSession($refreshToken = null) */ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null) { - $sessionToken = $sessionToken ?? $_COOKIE[SESSION_COOKIE] ?? null; - $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$sessionToken || !$refreshToken) { throw new \InvalidArgumentException('Session token and refresh token are required.'); @@ -117,7 +117,7 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu */ public function getClaims($token = null) { - $token = $token ?? $_COOKIE[SESSION_COOKIE] ?? null; + $token = $token ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; if (!$token) { throw new \InvalidArgumentException('Token is required.'); @@ -136,7 +136,7 @@ public function getClaims($token = null) */ public function getUserDetails(string $refreshToken = null) { - $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { throw new \InvalidArgumentException('Refresh token is required.'); @@ -158,7 +158,7 @@ public function getUserDetails(string $refreshToken = null) */ public function logout(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { throw new \InvalidArgumentException('Refresh token is required.'); @@ -181,7 +181,7 @@ public function logout(string $refreshToken = null): void */ public function logoutAll(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[REFRESH_COOKIE] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { throw new \InvalidArgumentException('Refresh token is required.'); diff --git a/src/SDK/EndpointsV1.php b/src/SDK/EndpointsV1.php index 40441e0..91e2b7f 100644 --- a/src/SDK/EndpointsV1.php +++ b/src/SDK/EndpointsV1.php @@ -12,6 +12,11 @@ const SESSION_COOKIE = "DS"; const REFRESH_COOKIE = "DSR"; +const SESSION_TOKEN = "sessionToken"; +const REFRESH_TOKEN = "refreshSessionToken"; + +const REDIRECT_LOCATION_NAME = "Location"; + const PHONE_REGEX = '/^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?){0,}((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$/'; class EndpointsV1 @@ -21,6 +26,11 @@ class EndpointsV1 public static $SESSION_COOKIE_NAME = SESSION_COOKIE; public static $REFRESH_COOKIE_NAME = REFRESH_COOKIE; + public static $SESSION_TOKEN_NAME = SESSION_TOKEN; + public static $REFRESH_TOKEN_NAME = REFRESH_TOKEN; + + public static $REDIRECT_LOCATION_COOKIE_NAME = REDIRECT_LOCATION_NAME; + public static $REFRESH_TOKEN_PATH; public static $SELECT_TENANT_PATH; public static $LOGOUT_PATH; From 13ea070f63b7ba7a2e2e875375e0e19744ac387e Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 18:15:11 -0700 Subject: [PATCH 08/13] fixed linter --- src/SDK/API.php | 8 ++++---- src/SDK/Auth/SSO.php | 26 ++++++++++++------------- src/SDK/DescopeSDK.php | 26 ++++++++++++------------- src/SDK/Management/AssociatedTenant.php | 4 ++-- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index 5a93b55..2b8a512 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -20,7 +20,7 @@ class API /** * Constructor for API class. * - * @param string $projectId + * @param string $projectId * @param string|null $managementKey Management key for authentication. */ public function __construct(string $projectId, ?string $managementKey) @@ -178,9 +178,9 @@ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken /** * Generates a JWT response array with the given parameters. * - * @param array $responseBody - * @param string|null $refreshToken Refresh token. - * @param string|null $audience Audience. + * @param array $responseBody + * @param string|null $refreshToken Refresh token. + * @param string|null $audience Audience. * @return array JWT response array. */ public function generateJwtResponse(array $responseBody, ?string $refreshToken = null, ?string $audience = null): array diff --git a/src/SDK/Auth/SSO.php b/src/SDK/Auth/SSO.php index 3f5bee6..49422ff 100644 --- a/src/SDK/Auth/SSO.php +++ b/src/SDK/Auth/SSO.php @@ -28,13 +28,13 @@ public function __construct(API $api) /** * SSO sign-in request. * - * @param string|null $tenant Tenant identifier. - * @param string|null $redirectUrl URL to redirect after authentication. - * @param string|null $prompt Prompt parameter. - * @param bool $stepup Whether to perform step-up authentication. - * @param bool $mfa Whether to enforce MFA. - * @param array $customClaims Custom claims to include in the token. - * @param string|null $ssoAppId SSO application identifier. + * @param string|null $tenant Tenant identifier. + * @param string|null $redirectUrl URL to redirect after authentication. + * @param string|null $prompt Prompt parameter. + * @param bool $stepup Whether to perform step-up authentication. + * @param bool $mfa Whether to enforce MFA. + * @param array $customClaims Custom claims to include in the token. + * @param string|null $ssoAppId SSO application identifier. * @return array Response array. * @throws AuthException */ @@ -61,7 +61,7 @@ public function signIn(?string $tenant = null, ?string $redirectUrl = null, ?str /** * Exchanges SSO code for authentication. * - * @param string|null $code The exchange code. + * @param string|null $code The exchange code. * @return array Response array. */ public function exchangeToken(?string $code = null): array @@ -77,9 +77,9 @@ public function exchangeToken(?string $code = null): array /** * Composes the SSO sign-in URL. * - * @param string|null $tenant Tenant identifier. - * @param string|null $redirectUrl Redirect URL. - * @param string|null $prompt Prompt parameter. + * @param string|null $tenant Tenant identifier. + * @param string|null $redirectUrl Redirect URL. + * @param string|null $prompt Prompt parameter. * @return string Composed URL. */ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string $prompt): string @@ -104,7 +104,7 @@ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string /** * Validates the tenant parameter. * - * @param string|null $tenant The tenant identifier. + * @param string|null $tenant The tenant identifier. * @throws AuthException */ private function validateTenant(?string $tenant): void @@ -117,7 +117,7 @@ private function validateTenant(?string $tenant): void /** * Validates the redirect URL parameter. * - * @param string|null $redirectUrl The redirect URL. + * @param string|null $redirectUrl The redirect URL. * @throws AuthException */ private function validateRedirectUrl(?string $redirectUrl): void diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index de9020a..67ac5a2 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -50,12 +50,12 @@ public function __construct(array $config) } /** - * Verify if the JWT is valid and not expired. - * - * @param string|null $sessionToken The session token to verify. - * @return bool Verification result. - * @throws AuthException - */ + * Verify if the JWT is valid and not expired. + * + * @param string|null $sessionToken The session token to verify. + * @return bool Verification result. + * @throws AuthException + */ public function verify($sessionToken = null) { $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME_NAME] ?? null; @@ -71,7 +71,7 @@ public function verify($sessionToken = null) /** * Refresh session token using the refresh token. * - * @param string|null $refreshToken The refresh token to use. + * @param string|null $refreshToken The refresh token to use. * @return array The new session information. * @throws AuthException */ @@ -90,8 +90,8 @@ public function refreshSession($refreshToken = null) /** * Verify and refresh the session using session and refresh tokens. * - * @param string|null $sessionToken The session token. - * @param string|null $refreshToken The refresh token. + * @param string|null $sessionToken The session token. + * @param string|null $refreshToken The refresh token. * @return array The refreshed session information. * @throws AuthException */ @@ -111,7 +111,7 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu /** * Get the JWT claims if the token is valid. * - * @param string|null $token The token to extract claims from. + * @param string|null $token The token to extract claims from. * @return array The JWT claims. * @throws AuthException */ @@ -130,7 +130,7 @@ public function getClaims($token = null) /** * Retrieve user details using the refresh token. * - * @param string|null $refreshToken The refresh token of the user. + * @param string|null $refreshToken The refresh token of the user. * @return array The user details. * @throws AuthException */ @@ -152,7 +152,7 @@ public function getUserDetails(string $refreshToken = null) /** * Logout a user using the refresh token. * - * @param string|null $refreshToken The refresh token of the user. + * @param string|null $refreshToken The refresh token of the user. * @return void * @throws AuthException */ @@ -175,7 +175,7 @@ public function logout(string $refreshToken = null): void /** * Logout a user from all devices using the refresh token. * - * @param string|null $refreshToken The refresh token of the user. + * @param string|null $refreshToken The refresh token of the user. * @return void * @throws AuthException */ diff --git a/src/SDK/Management/AssociatedTenant.php b/src/SDK/Management/AssociatedTenant.php index 800e96a..2ff3585 100644 --- a/src/SDK/Management/AssociatedTenant.php +++ b/src/SDK/Management/AssociatedTenant.php @@ -29,9 +29,9 @@ class AssociatedTenant /** * Constructor for the AssociatedTenant class. * - * @param string $tenantId The Tenant ID. + * @param string $tenantId The Tenant ID. * @param array $roleNames The role names for the user in the tenant. - * @param array $roleIds The role IDs for the user in the tenant. + * @param array $roleIds The role IDs for the user in the tenant. */ public function __construct(string $tenantId, array $roleNames = [], array $roleIds = []) { From c47cc7c3d155d53b2d7ddac9046c2f031e876ee8 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 18:17:50 -0700 Subject: [PATCH 09/13] fixed linter --- src/SDK/Auth/Password.php | 2 +- src/SDK/Auth/SSO.php | 14 +++++++------- src/SDK/Management/AssociatedTenant.php | 2 +- src/tests/Management/UserTest.php | 2 -- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/SDK/Auth/Password.php b/src/SDK/Auth/Password.php index 1b5659f..a6bbea8 100644 --- a/src/SDK/Auth/Password.php +++ b/src/SDK/Auth/Password.php @@ -57,7 +57,7 @@ public function signUp(string $loginId, string $password, ?array $user = null, ? * @param string $loginId Login ID of the user. * @param string $password Password of the user. * @return array JWT response array. - * @throws AuthException + * @throws AuthException */ public function signIn(string $loginId, string $password): array { diff --git a/src/SDK/Auth/SSO.php b/src/SDK/Auth/SSO.php index 49422ff..f71a803 100644 --- a/src/SDK/Auth/SSO.php +++ b/src/SDK/Auth/SSO.php @@ -61,7 +61,7 @@ public function signIn(?string $tenant = null, ?string $redirectUrl = null, ?str /** * Exchanges SSO code for authentication. * - * @param string|null $code The exchange code. + * @param string|null $code The exchange code. * @return array Response array. */ public function exchangeToken(?string $code = null): array @@ -77,9 +77,9 @@ public function exchangeToken(?string $code = null): array /** * Composes the SSO sign-in URL. * - * @param string|null $tenant Tenant identifier. - * @param string|null $redirectUrl Redirect URL. - * @param string|null $prompt Prompt parameter. + * @param string|null $tenant Tenant identifier. + * @param string|null $redirectUrl Redirect URL. + * @param string|null $prompt Prompt parameter. * @return string Composed URL. */ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string $prompt): string @@ -104,8 +104,8 @@ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string /** * Validates the tenant parameter. * - * @param string|null $tenant The tenant identifier. - * @throws AuthException + * @param string|null $tenant The tenant identifier. + * @throws AuthException */ private function validateTenant(?string $tenant): void { @@ -117,7 +117,7 @@ private function validateTenant(?string $tenant): void /** * Validates the redirect URL parameter. * - * @param string|null $redirectUrl The redirect URL. + * @param string|null $redirectUrl The redirect URL. * @throws AuthException */ private function validateRedirectUrl(?string $redirectUrl): void diff --git a/src/SDK/Management/AssociatedTenant.php b/src/SDK/Management/AssociatedTenant.php index 2ff3585..0b3cb2b 100644 --- a/src/SDK/Management/AssociatedTenant.php +++ b/src/SDK/Management/AssociatedTenant.php @@ -53,4 +53,4 @@ public function toArray(): array 'roleIds' => $this->roleIds, ]; } -} \ No newline at end of file +} diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index 9601ffb..96d2558 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -260,5 +260,3 @@ public function testDeactivateUser() print_r($response); } } - - From a0f1ccc8e83fb892e16afef12cc8a49788ce5f54 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 18:21:52 -0700 Subject: [PATCH 10/13] fixed linter --- sample/dashboard.php | 18 ++++++++---------- src/tests/Management/UserTest.php | 4 ++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/sample/dashboard.php b/sample/dashboard.php index 626083a..37af5a8 100644 --- a/sample/dashboard.php +++ b/sample/dashboard.php @@ -7,17 +7,15 @@ $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..'); $dotenv->load(); -if (!isset($_ENV['DESCOPE_PROJECT_ID'])) { - echo "Descope Project ID not present. Please check .env file."; - exit(1); -} - -$descopeSDK = new DescopeSDK([ - 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], - 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'] -]); - +// if (!isset($_ENV['DESCOPE_PROJECT_ID'])) { +// echo "Descope Project ID not present. Please check .env file."; +// exit(1); +// } +// $descopeSDK = new DescopeSDK([ +// 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], +// 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'] +// ]); if (!isset($_SESSION["user"])) { session_destroy(); diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index 96d2558..cad13c4 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -24,8 +24,8 @@ class UserPasswordTest extends TestCase protected function setUp(): void { $config = [ - 'projectId' => 'P2OkfVnJi5Ht7mpCqHjx17nV5epH', - 'managementKey' => 'K2nmY9zuewgPZpRpTAmGW6hlBYYp6EiZfXeMkJyHDg89NOpobIlMbSZzb388BiilhPfg04l', + 'projectId' => 'YOUR_PROJECT_ID', + 'managementKey' => 'YOUR_MANAGEMENT_KEY', ]; $this->descopeSDK = new DescopeSDK($config); From d5619aad5d80a44eb481ab10730b4b2b487e11a3 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 21:58:40 -0700 Subject: [PATCH 11/13] fixed search --- sample/callback.php | 4 +- sample/dashboard.php | 11 - sample/login.php | 6 + src/SDK/API.php | 119 ++++-- src/SDK/EndpointsV1.php | 4 + src/SDK/Management/AssociatedTenant.php | 13 +- src/SDK/Management/Management.php | 12 + src/SDK/Management/MgmtV1.php | 7 +- src/SDK/Management/Password/UserPassword.php | 56 +++ .../Password/UserPasswordBcrypt.php | 40 ++ .../Password/UserPasswordDjango.php | 40 ++ .../Password/UserPasswordFirebase.php | 66 ++++ .../Password/UserPasswordPHPass.php | 52 +++ .../Password/UserPasswordPbkdf2.php | 56 +++ src/SDK/Management/User.php | 365 +++++++++++++----- src/SDK/Management/UserPassword.php | 273 ------------- src/tests/Management/AuditTest.php | 128 +++--- src/tests/Management/UserPwdTest.php | 12 +- src/tests/Management/UserTest.php | 124 +++--- 19 files changed, 810 insertions(+), 578 deletions(-) create mode 100644 src/SDK/Management/Password/UserPassword.php create mode 100644 src/SDK/Management/Password/UserPasswordBcrypt.php create mode 100644 src/SDK/Management/Password/UserPasswordDjango.php create mode 100644 src/SDK/Management/Password/UserPasswordFirebase.php create mode 100644 src/SDK/Management/Password/UserPasswordPHPass.php create mode 100644 src/SDK/Management/Password/UserPasswordPbkdf2.php delete mode 100644 src/SDK/Management/UserPassword.php diff --git a/sample/callback.php b/sample/callback.php index c53c5c6..7e1da49 100644 --- a/sample/callback.php +++ b/sample/callback.php @@ -17,10 +17,10 @@ echo "Descope Project ID not present. Please check .env file."; exit(1); } - + $descopeSDK = new DescopeSDK([ 'projectId' => $_ENV['DESCOPE_PROJECT_ID'] - ]); + ]); if (isset($_POST["sessionToken"])) { if ($descopeSDK->verify($_POST["sessionToken"])) { diff --git a/sample/dashboard.php b/sample/dashboard.php index 37af5a8..527189e 100644 --- a/sample/dashboard.php +++ b/sample/dashboard.php @@ -1,22 +1,11 @@ load(); -// if (!isset($_ENV['DESCOPE_PROJECT_ID'])) { -// echo "Descope Project ID not present. Please check .env file."; -// exit(1); -// } - -// $descopeSDK = new DescopeSDK([ -// 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], -// 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'] -// ]); - if (!isset($_SESSION["user"])) { session_destroy(); header('Location: login.php'); diff --git a/sample/login.php b/sample/login.php index 9f177ba..8e37477 100644 --- a/sample/login.php +++ b/sample/login.php @@ -37,6 +37,10 @@ function sendFormData(sessionToken, userDetails) { return user; } + async function handleLogout() { + + } + async function handleLogin() { try { console.log("Attempting to refresh the session..."); @@ -57,6 +61,7 @@ function sendFormData(sessionToken, userDetails) { sendFormData(sessionToken, user.data); } catch (error) { console.log("Error during login:", error); + sdk.logout(); window.location.href = 'login.php'; // Redirect to login on error } } @@ -68,6 +73,7 @@ function sendFormData(sessionToken, userDetails) { console.log("Valid refresh token found. Logging in..."); handleLogin(); } else { + sdk.logout(); console.log("No valid refresh token. Displaying login form."); const container = document.getElementById("container") container.innerHTML = ''; diff --git a/src/SDK/API.php b/src/SDK/API.php index 2b8a512..717f669 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -20,7 +20,7 @@ class API /** * Constructor for API class. * - * @param string $projectId + * @param string $projectId * @param string|null $managementKey Management key for authentication. */ public function __construct(string $projectId, ?string $managementKey) @@ -52,7 +52,7 @@ public function __construct(string $projectId, ?string $managementKey) * This function ensures that empty arrays in the input data are * converted to empty objects (stdClass) before being JSON encoded. * - * @param mixed $data The data to transform, which can be an array or any other type. + * @param mixed $data The data to transform, which can be an array or any other type. * @return mixed The transformed data with empty arrays replaced by empty objects. */ private function transformEmptyArraysToObjects($data) @@ -76,9 +76,9 @@ private function transformEmptyArraysToObjects($data) /** * Requests JwtResponse from Descope APIs with the given body and auth token. * - * @param string $uri URI endpoint. - * @param array $body Request body. - * @param bool $useManagementKey Whether to use the management key for authentication. + * @param string $uri URI endpoint. + * @param array $body Request body. + * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. * @throws AuthException|GuzzleException|\JsonException If the request fails. */ @@ -130,8 +130,8 @@ public function doPost(string $uri, array $body, ?bool $useManagementKey = false /** * Sends a GET request to the specified URI with an optional auth token. * - * @param string $uri URI endpoint. - * @param bool $useManagementKey Whether to use the management key for authentication. + * @param string $uri URI endpoint. + * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. * @throws AuthException|GuzzleException|\JsonException If the request fails. */ @@ -175,12 +175,53 @@ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken } } + /** + * Sends a DELETE request to the specified URI with an auth token. + * + * @param string $uri URI endpoint. + * @return array JWT response array. + * @throws AuthException|GuzzleException|\JsonException If the request fails. + */ + public function doDelete(string $uri): array + { + $authToken = $this->getAuthToken(true); + + try { + $response = $this->httpClient->delete( + $uri, + [ + 'headers' => $this->getHeaders($authToken), + ] + ); + + // Ensure the response is an object with getBody method + if (!is_object($response) || !method_exists($response, 'getBody') || !method_exists($response, 'getHeader')) { + throw new AuthException(500, 'internal error', 'Invalid response from API'); + } + + // Read Body + $body = $response->getBody(); + $body->rewind(); + $contents = $body->getContents() ?? []; + + return json_decode($contents, true, 512, JSON_THROW_ON_ERROR); + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + echo "Error: HTTP Status Code: $statusCode, Response: $responseBody"; + return [ + 'statusCode' => $statusCode, + 'response' => $responseBody, + ]; + } + } + /** * Generates a JWT response array with the given parameters. * - * @param array $responseBody - * @param string|null $refreshToken Refresh token. - * @param string|null $audience Audience. + * @param array $responseBody + * @param string|null $refreshToken Refresh token. + * @param string|null $audience Audience. * @return array JWT response array. */ public function generateJwtResponse(array $responseBody, ?string $refreshToken = null, ?string $audience = null): array @@ -196,7 +237,7 @@ public function generateJwtResponse(array $responseBody, ?string $refreshToken = /** * Generates headers for the HTTP request. * - * @param string|null $authToken Authentication token. + * @param string|null $authToken Authentication token. * @return array Headers array. */ private function getHeaders(string $authToken): array @@ -214,7 +255,7 @@ private function getHeaders(string $authToken): array /** * Constructs the auth token based on whether the management key is used. * - * @param bool $useManagementKey Whether to use the management key for authentication. + * @param bool $useManagementKey Whether to use the management key for authentication. * @return string The constructed auth token. */ private function getAuthToken(bool $useManagementKey, ?string $refreshToken = null): string @@ -230,27 +271,39 @@ private function getAuthToken(bool $useManagementKey, ?string $refreshToken = nu return $this->projectId; } + /** + * Generates authentication information from the response body. + * + * This method processes the response body to extract JWTs, session data, + * and cookie settings, and adjusts properties based on the token type. + * + * @param array $responseBody The API response body containing JWTs and user data. + * @param string|null $refreshToken Optional refresh token. + * @param bool $userJwt Indicates if user-related JWT information should be processed. + * @param string|null $audience Optional audience identifier. + * @return array The structured JWT response array containing session and user data. + */ private function generateAuthInfo(array $responseBody, ?string $refreshToken, bool $userJwt, ?string $audience): array { $jwtResponse = []; $stJwt = $responseBody['sessionJwt'] ?? ''; if ($stJwt) { - $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME] = $stJwt; + $jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME] = $stJwt; } $rtJwt = $responseBody['refreshJwt'] ?? ''; if ($refreshToken) { - $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME] = $refreshToken; + $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME] = $refreshToken; } elseif ($rtJwt) { - $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME] = $rtJwt; + $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME] = $rtJwt; } $jwtResponse = $this->adjustProperties($jwtResponse, $userJwt); if ($userJwt) { - $jwtResponse[EndpointsV1::COOKIE_DATA_NAME] = [ + $jwtResponse[EndpointsV1::$COOKIE_DATA_NAME] = [ 'exp' => $responseBody['cookieExpiration'] ?? 0, 'maxAge' => $responseBody['cookieMaxAge'] ?? 0, 'domain' => $responseBody['cookieDomain'] ?? '', @@ -261,31 +314,41 @@ private function generateAuthInfo(array $responseBody, ?string $refreshToken, bo return $jwtResponse; } + /** + * Adjusts properties of the JWT response array. + * + * This method sets permissions, roles, and tenant data from the JWT + * and processes the issuer and subject values to extract project and user IDs. + * + * @param array $jwtResponse The JWT response array to adjust. + * @param bool $userJwt Indicates if user-related JWT information should be processed. + * @return array The adjusted JWT response array with updated properties. + */ private function adjustProperties(array $jwtResponse, bool $userJwt): array { - if (isset($jwtResponse[EndpointsV1::SESSION_TOKEN_NAME])) { - $jwtResponse['permissions'] = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['permissions'] ?? []; - $jwtResponse['roles'] = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['roles'] ?? []; - $jwtResponse['tenants'] = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['tenants'] ?? []; - } elseif (isset($jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME])) { - $jwtResponse['permissions'] = $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['permissions'] ?? []; - $jwtResponse['roles'] = $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['roles'] ?? []; - $jwtResponse['tenants'] = $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['tenants'] ?? []; + if (isset($jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME])) { + $jwtResponse['permissions'] = $jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME]['permissions'] ?? []; + $jwtResponse['roles'] = $jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME]['roles'] ?? []; + $jwtResponse['tenants'] = $jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME]['tenants'] ?? []; + } elseif (isset($jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME])) { + $jwtResponse['permissions'] = $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME]['permissions'] ?? []; + $jwtResponse['roles'] = $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME]['roles'] ?? []; + $jwtResponse['tenants'] = $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME]['tenants'] ?? []; } else { $jwtResponse['permissions'] = $jwtResponse['permissions'] ?? []; $jwtResponse['roles'] = $jwtResponse['roles'] ?? []; $jwtResponse['tenants'] = $jwtResponse['tenants'] ?? []; } - $issuer = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['iss'] ?? - $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['iss'] ?? + $issuer = $jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME]['iss'] ?? + $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME]['iss'] ?? $jwtResponse['iss'] ?? ''; $issuerParts = explode("/", $issuer); $jwtResponse['projectId'] = end($issuerParts); - $sub = $jwtResponse[EndpointsV1::SESSION_TOKEN_NAME]['sub'] ?? - $jwtResponse[EndpointsV1::REFRESH_TOKEN_NAME]['sub'] ?? + $sub = $jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME]['sub'] ?? + $jwtResponse[EndpointsV1::$REFRESH_TOKEN_NAME]['sub'] ?? $jwtResponse['sub'] ?? ''; if ($userJwt) { diff --git a/src/SDK/EndpointsV1.php b/src/SDK/EndpointsV1.php index 91e2b7f..6549681 100644 --- a/src/SDK/EndpointsV1.php +++ b/src/SDK/EndpointsV1.php @@ -15,6 +15,8 @@ const SESSION_TOKEN = "sessionToken"; const REFRESH_TOKEN = "refreshSessionToken"; +const COOKIE_DATA = "cookieData"; + const REDIRECT_LOCATION_NAME = "Location"; const PHONE_REGEX = '/^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?){0,}((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$/'; @@ -29,6 +31,8 @@ class EndpointsV1 public static $SESSION_TOKEN_NAME = SESSION_TOKEN; public static $REFRESH_TOKEN_NAME = REFRESH_TOKEN; + public static $COOKIE_DATA_NAME = COOKIE_DATA; + public static $REDIRECT_LOCATION_COOKIE_NAME = REDIRECT_LOCATION_NAME; public static $REFRESH_TOKEN_PATH; diff --git a/src/SDK/Management/AssociatedTenant.php b/src/SDK/Management/AssociatedTenant.php index 0b3cb2b..a4a3b93 100644 --- a/src/SDK/Management/AssociatedTenant.php +++ b/src/SDK/Management/AssociatedTenant.php @@ -19,13 +19,6 @@ class AssociatedTenant */ public $roleNames = []; - /** - * Represents the role IDs for a user in the Tenant. - * - * @var array The Role IDs. - */ - public $roleIds = []; - /** * Constructor for the AssociatedTenant class. * @@ -33,11 +26,10 @@ class AssociatedTenant * @param array $roleNames The role names for the user in the tenant. * @param array $roleIds The role IDs for the user in the tenant. */ - public function __construct(string $tenantId, array $roleNames = [], array $roleIds = []) + public function __construct(string $tenantId, array $roleNames = []) { $this->tenantId = $tenantId; $this->roleNames = $roleNames; - $this->roleIds = $roleIds; } /** @@ -49,8 +41,7 @@ public function toArray(): array { return [ 'tenantId' => $this->tenantId, - 'roleNames' => $this->roleNames, - 'roleIds' => $this->roleIds, + 'roleNames' => $this->roleNames ]; } } diff --git a/src/SDK/Management/Management.php b/src/SDK/Management/Management.php index 77899b8..b536edd 100644 --- a/src/SDK/Management/Management.php +++ b/src/SDK/Management/Management.php @@ -16,6 +16,7 @@ class Management * @var User The User management component. */ public User $user; + public Audit $audit; /** * Constructor for Management class. @@ -25,6 +26,7 @@ class Management public function __construct(API $auth) { $this->user = new User($auth); + $this->audit = new Audit($auth); } /** @@ -36,4 +38,14 @@ public function user(): User { return $this->user; } + + /** + * Get the Audit Management component. + * + * @return Audit The Audit management instance. + */ + public function audit(): Audit + { + return $this->audit; + } } diff --git a/src/SDK/Management/MgmtV1.php b/src/SDK/Management/MgmtV1.php index c4e4254..1033da1 100644 --- a/src/SDK/Management/MgmtV1.php +++ b/src/SDK/Management/MgmtV1.php @@ -5,7 +5,6 @@ use Descope\SDK\EndpointsV1; - const DEFAULT_URL_PREFIX = "https://api"; const DEFAULT_DOMAIN = "descope.com"; @@ -96,6 +95,8 @@ class MgmtV1 public static string $TENANT_DELETE_PATH; public static string $TENANT_UPDATE_PATH; public static string $TENANT_CREATE_PATH; + public static string $AUDIT_SEARCH; + public static string $AUDIT_CREATE_EVENT; /** * Sets the base URL based on the project ID, taking into account the region. @@ -232,6 +233,10 @@ private static function updatePaths(): void self::$FLOW_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/flow/export"; self::$TEMPLATE_IMPORT_PATH = self::$baseUrl . "/v1/mgmt/template/import"; self::$TEMPLATE_EXPORT_PATH = self::$baseUrl . "/v1/mgmt/template/export"; + + // Audit + self::$AUDIT_SEARCH = self::$baseUrl . "/v1/mgmt/audit/search"; + self::$AUDIT_CREATE_EVENT = self::$baseUrl . "/v1/mgmt/audit/event"; } } diff --git a/src/SDK/Management/Password/UserPassword.php b/src/SDK/Management/Password/UserPassword.php new file mode 100644 index 0000000..e1d0b26 --- /dev/null +++ b/src/SDK/Management/Password/UserPassword.php @@ -0,0 +1,56 @@ +cleartext = $cleartext; + $this->hashed = $hashed; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + $data = []; + if ($this->cleartext !== null) { + $data['cleartext'] = $this->cleartext; + } + if ($this->hashed !== null) { + $data['hashed'] = $this->hashed->toArray(); + } + return $data; + } +} diff --git a/src/SDK/Management/Password/UserPasswordBcrypt.php b/src/SDK/Management/Password/UserPasswordBcrypt.php new file mode 100644 index 0000000..c166168 --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordBcrypt.php @@ -0,0 +1,40 @@ +hash = $hash; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'bcrypt' => [ + 'hash' => $this->hash, + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Management/Password/UserPasswordDjango.php b/src/SDK/Management/Password/UserPasswordDjango.php new file mode 100644 index 0000000..4d2528c --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordDjango.php @@ -0,0 +1,40 @@ +hash = $hash; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'django' => [ + 'hash' => $this->hash, + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Management/Password/UserPasswordFirebase.php b/src/SDK/Management/Password/UserPasswordFirebase.php new file mode 100644 index 0000000..2ee4339 --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordFirebase.php @@ -0,0 +1,66 @@ +hash = $hash; + $this->salt = $salt; + $this->saltSeparator = $saltSeparator; + $this->signerKey = $signerKey; + $this->memory = $memory; + $this->rounds = $rounds; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'firebase' => [ + 'hash' => $this->hash, + 'salt' => $this->salt, + 'saltSeparator' => $this->saltSeparator, + 'signerKey' => $this->signerKey, + 'memory' => $this->memory, + 'rounds' => $this->rounds, + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Management/Password/UserPasswordPHPass.php b/src/SDK/Management/Password/UserPasswordPHPass.php new file mode 100644 index 0000000..33a4257 --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordPHPass.php @@ -0,0 +1,52 @@ +hash = $hash; + $this->salt = $salt; + $this->iterations = $iterations; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'phpass' => [ + 'hash' => $this->hash, + 'salt' => $this->salt, + 'iterations' => $this->iterations + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Management/Password/UserPasswordPbkdf2.php b/src/SDK/Management/Password/UserPasswordPbkdf2.php new file mode 100644 index 0000000..1de37dd --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordPbkdf2.php @@ -0,0 +1,56 @@ +hash = $hash; + $this->salt = $salt; + $this->iterations = $iterations; + $this->variant = $variant; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'pbkdf2' => [ + 'hash' => $this->hash, + 'salt' => $this->salt, + 'iterations' => $this->iterations, + 'type' => $this->variant, + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Management/User.php b/src/SDK/Management/User.php index 5ad9a27..c495b0d 100644 --- a/src/SDK/Management/User.php +++ b/src/SDK/Management/User.php @@ -8,10 +8,13 @@ use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\MgmtV1; use Descope\SDK\Management\LoginOptions; -use Descope\SDK\Management\UserPassword; +use Descope\SDK\Management\Password\UserPassword; use Descope\SDK\API; use GuzzleHttp\Exception\RequestException; +/** + * UserObj class represents the details of a user. + */ class UserObj { public string $loginId; @@ -31,6 +34,26 @@ class UserObj public ?array $ssoAppIds; public ?UserPassword $password; + /** + * Constructor for UserObj. + * + * @param string $loginId The user's login ID. + * @param string|null $email The user's email address. + * @param string|null $phone The user's phone number. + * @param string|null $displayName The user's display name. + * @param string|null $givenName The user's given name. + * @param string|null $middleName The user's middle name. + * @param string|null $familyName The user's family name. + * @param array|null $roleNames The roles assigned to the user. + * @param array|null $userTenants The tenants associated with the user. + * @param string|null $picture The URL of the user's profile picture. + * @param array|null $customAttributes Custom attributes associated with the user. + * @param bool|null $verifiedEmail Whether the user's email is verified. + * @param bool|null $verifiedPhone Whether the user's phone number is verified. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password The user's password. + */ public function __construct( string $loginId, ?string $email = null, @@ -68,15 +91,46 @@ public function __construct( } } +/** + * User class provides methods to interact with user-related functionalities in the Descope API. + */ class User { private API $api; + /** + * Constructor for the User class. + * + * @param API $api The API instance to be used for HTTP requests. + */ public function __construct(API $api) { $this->api = $api; } + /** + * Creates a new user. + * + * @param string $loginId The login ID for the user. + * @param string|null $email The user's email address. + * @param string|null $phone The user's phone number. + * @param string|null $displayName The user's display name. + * @param string|null $givenName The user's given name. + * @param string|null $middleName The user's middle name. + * @param string|null $familyName The user's family name. + * @param string|null $picture The user's profile picture URL. + * @param array|null $customAttributes Custom attributes for the user. + * @param bool|null $verifiedEmail Indicates if the user's email is verified. + * @param bool|null $verifiedPhone Indicates if the user's phone is verified. + * @param string|null $inviteUrl URL to invite the user. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password The user's password. + * @param array|null $roleNames Roles assigned to the user. + * @param array|null $userTenants Tenants associated with the user. + * @return array The created user's information. + * @throws AuthException + */ public function create( string $loginId, ?string $email = null, @@ -129,6 +183,29 @@ public function create( return $this->api->generateJwtResponse($response); } + /** + * Creates a test user. + * + * @param string $loginId The login ID for the test user. + * @param string|null $email The user's email address. + * @param string|null $phone The user's phone number. + * @param string|null $displayName The user's display name. + * @param string|null $givenName The user's given name. + * @param string|null $middleName The user's middle name. + * @param string|null $familyName The user's family name. + * @param string|null $picture The user's profile picture URL. + * @param array|null $customAttributes Custom attributes for the user. + * @param bool|null $verifiedEmail Indicates if the user's email is verified. + * @param bool|null $verifiedPhone Indicates if the user's phone is verified. + * @param string|null $inviteUrl URL to invite the user. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password The user's password. + * @param array|null $roleNames Roles assigned to the user. + * @param array|null $userTenants Tenants associated with the user. + * @return array The created test user's information. + * @throws AuthException + */ public function createTestUser( string $loginId, ?string $email = null, @@ -181,6 +258,31 @@ public function createTestUser( return $this->api->generateJwtResponse($response); } + /** + * Invites a user. + * + * @param string $loginId The login ID for the user. + * @param string|null $email The user's email address. + * @param string|null $phone The user's phone number. + * @param string|null $displayName The user's display name. + * @param string|null $givenName The user's given name. + * @param string|null $middleName The user's middle name. + * @param string|null $familyName The user's family name. + * @param string|null $picture The user's profile picture URL. + * @param array|null $customAttributes Custom attributes for the user. + * @param bool|null $verifiedEmail Indicates if the user's email is verified. + * @param bool|null $verifiedPhone Indicates if the user's phone is verified. + * @param string|null $inviteUrl URL to invite the user. + * @param bool|null $sendMail Indicates if the invite should be sent via email. + * @param bool|null $sendSms Indicates if the invite should be sent via SMS. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password The user's password. + * @param array|null $roleNames Roles assigned to the user. + * @param array|null $userTenants Tenants associated with the user. + * @return array The invited user's information. + * @throws AuthException + */ public function invite( string $loginId, ?string $email = null, @@ -235,6 +337,16 @@ public function invite( return $this->api->generateJwtResponse($response); } + /** + * Invites a batch of users. + * + * @param array $users The array of UserObj instances representing users to be invited. + * @param string|null $inviteUrl URL to invite the users. + * @param bool|null $sendMail Indicates if the invite should be sent via email. + * @param bool|null $sendSms Indicates if the invite should be sent via SMS. + * @return array The response containing details of the invited users. + * @throws AuthException + */ public function inviteBatch( array $users, ?string $inviteUrl = null, @@ -254,6 +366,28 @@ public function inviteBatch( return $this->api->generateJwtResponse($response); } + /** + * Updates an existing user's details. + * + * @param string $loginId The login ID of the user to update. + * @param string|null $email The user's new email address. + * @param string|null $phone The user's new phone number. + * @param string|null $displayName The user's new display name. + * @param string|null $givenName The user's new given name. + * @param string|null $middleName The user's new middle name. + * @param string|null $familyName The user's new family name. + * @param string|null $picture The user's new profile picture URL. + * @param array|null $customAttributes Updated custom attributes for the user. + * @param bool|null $verifiedEmail Indicates if the user's email is verified. + * @param bool|null $verifiedPhone Indicates if the user's phone is verified. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password The user's new password. + * @param array|null $roleNames Updated roles for the user. + * @param array|null $userTenants Updated tenants associated with the user. + * @return void + * @throws AuthException + */ public function update( string $loginId, ?string $email = null, @@ -266,9 +400,8 @@ public function update( ?array $customAttributes = null, ?bool $verifiedEmail = null, ?bool $verifiedPhone = null, - ?array $additionalLoginIds = null, + ?array $additionalIdentifiers = null, ?array $ssoAppIds = null, - ?UserPassword $password = null, ?array $roleNames = null, ?array $userTenants = null ): void { @@ -287,14 +420,12 @@ public function update( $familyName, $roleNames, $userTenants, - false, $picture, $customAttributes, $verifiedEmail, $verifiedPhone, - $additionalLoginIds, - $ssoAppIds, - $password + $additionalIdentifiers, + $ssoAppIds ), true ); @@ -303,7 +434,7 @@ public function update( /** * Delete an existing user by login ID. IMPORTANT: This action is irreversible. Use carefully. * - * @param string $loginId The login ID from the user's JWT. + * @param string $loginId The login ID from the user's JWT. * @return void * @throws AuthException if the delete operation fails. */ @@ -319,9 +450,9 @@ public function delete(string $loginId): void /** * Delete an existing user by user ID. IMPORTANT: This action is irreversible. Use carefully. * - * @param string $userId The user ID from the user's JWT. + * @param string $userId The user ID from the user's JWT. * @return void - * @throws AuthException if the delete operation fails. + * @throws AuthException */ public function deleteByUserId(string $userId): void { @@ -332,7 +463,13 @@ public function deleteByUserId(string $userId): void ); } - + /** + * Deletes all test users in the system. + * IMPORTANT: This action is irreversible. Use with caution. + * + * @return void + * @throws AuthException + */ public function deleteAllTestUsers(): void { $this->api->doDelete( @@ -342,8 +479,12 @@ public function deleteAllTestUsers(): void } /** + * Loads user details using the login ID. + * + * @param string $loginId The login ID of the user to retrieve. + * @return array The user's details. * @throws AuthException - */ + */ public function load(string $loginId): array { return $this->api->doGet( @@ -352,6 +493,13 @@ public function load(string $loginId): array ); } + /** + * Loads user details using the user ID. + * + * @param string $userId The user ID of the user to retrieve. + * @return array The user's details. + * @throws AuthException + */ public function loadByUserId(string $userId): array { return $this->api->doGet( @@ -397,6 +545,12 @@ public function searchAll( // Initialize arrays if they are null $tenantIds = $tenantIds ?? []; $roleNames = $roleNames ?? []; + $statuses = $statuses ?? []; + $emails = $emails ?? []; + $phones = $phones ?? []; + $ssoAppIds = $ssoAppIds ?? []; + $sort = $sort ?? []; + $customAttributes = $customAttributes ?? (object)[]; if ($limit < 0) { throw new AuthException( @@ -414,55 +568,26 @@ public function searchAll( ); } + // Prepare the request body $body = [ + 'loginId' => '', 'tenantIds' => $tenantIds, 'roleNames' => $roleNames, - 'limit' => $limit, - 'page' => $page, - 'testUsersOnly' => $testUsersOnly, - 'withTestUser' => $withTestUser, - 'customAttributes' => $customAttributes ?? (object)[], + 'limit' => (string)$limit, + 'page' => (string)$page, + 'text' => $text ?? '', + 'ssoOnly' => '', + 'withTestUser' => $withTestUser ? true : false, + 'testUsersOnly' => $testUsersOnly ? true : false, + 'customAttributes' => $customAttributes, + 'statuses' => $statuses, + 'emails' => $emails, + 'phones' => $phones, + 'ssoAppIds' => $ssoAppIds, + 'sort' => $sort, + 'loginIds' => [], ]; - $allowedStatuses = ['enabled', 'disabled', 'invited']; - if ($statuses !== null) { - foreach ($statuses as $status) { - if (!in_array($status, $allowedStatuses, true)) { - throw new AuthException( - 400, - 'ERROR_TYPE_INVALID_ARGUMENT', - "The status '$status' is invalid. Allowed values are: " . implode(", ", $allowedStatuses) - ); - } - } - } - - if ($emails !== null) { - $body['emails'] = $emails; - } - - if ($phones !== null) { - $body['phones'] = $phones; - } - - if ($customAttributes !== null) { - $body['customAttributes'] = $customAttributes; - } - - if ($ssoAppIds !== null) { - $body['ssoAppIds'] = $ssoAppIds; - } - - if ($text !== null) { - $body['text'] = $text; - } - - if ($sort !== null) { - $body['sort'] = $this->sortToArray($sort); - } - - $jsonBody = json_encode($body); - try { return $this->api->doPost( MgmtV1::$USERS_SEARCH_PATH, @@ -1234,6 +1359,37 @@ public function history(array $userIds): array { } } + /** + * Composes the request body for creating a user. + * + * This method structures the user information, including login ID, email, phone, + * display name, roles, tenants, and additional attributes, into an array format + * suitable for creating a user via the API. It also handles password assignment + * and the inclusion of optional properties such as verified email and phone. + * + * @param string $loginId The unique login ID for the user. + * @param string|null $email The user's email address. + * @param string|null $phone The user's phone number. + * @param string|null $displayName The user's display name. + * @param string|null $givenName The user's given (first) name. + * @param string|null $middleName The user's middle name. + * @param string|null $familyName The user's family (last) name. + * @param array $roleNames An array of role names assigned to the user. + * @param array $userTenants An array of user tenants with roles. + * @param bool $invited Flag to indicate if the user is invited. + * @param bool $test Flag to indicate if the user is a test user. + * @param string|null $picture URL of the user's profile picture. + * @param array|null $customAttributes Additional custom attributes for the user. + * @param bool|null $verifiedEmail Flag to indicate if the user's email is verified. + * @param bool|null $verifiedPhone Flag to indicate if the user's phone is verified. + * @param string|null $inviteUrl Optional URL for inviting the user. + * @param bool|null $sendMail Flag to send an invitation email. + * @param bool|null $sendSms Flag to send an invitation SMS. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password User's password information (cleartext or hashed). + * @return array The composed request body for user creation. + */ public function composeCreateBody( string $loginId, ?string $email, @@ -1293,6 +1449,20 @@ public function composeCreateBody( return $res; } + /** + * Composes the request body for batch user creation. + * + * This method iterates over a list of user objects, converting each into an array + * using the `composeCreateBody` method. It includes user details such as login ID, + * email, phone, and additional attributes. The resulting array of users is formatted + * for a batch creation API request. + * + * @param array $users The array of user objects to be created. + * @param string|null $inviteUrl Optional URL for sending invitations to the created users. + * @param bool|null $sendMail Optional flag indicating whether to send an invitation email. + * @param bool|null $sendSms Optional flag indicating whether to send an invitation SMS. + * @return array The structured request body for creating multiple users. + */ public function composeCreateBatchBody( array $users, ?string $inviteUrl, @@ -1329,6 +1499,31 @@ public function composeCreateBatchBody( return ['users' => $userArr]; } + /** + * Composes the request body for updating a user's information. + * + * This method creates a structured array with user details, including login ID, + * email, phone, display name, roles, tenants, and optional attributes like profile + * picture and custom attributes. It is used when sending a request to update user data. + * + * @param string $loginId The login ID of the user. + * @param string|null $email The user's email address. + * @param string|null $phone The user's phone number. + * @param string|null $displayName The user's display name. + * @param string|null $givenName The user's given (first) name. + * @param string|null $middleName The user's middle name. + * @param string|null $familyName The user's family (last) name. + * @param array|null $roleNames An array of role names assigned to the user. + * @param array|null $userTenants An array of user tenants with roles. + * @param string|null $picture URL of the user's profile picture. + * @param array|null $customAttributes Additional custom attributes for the user. + * @param bool|null $verifiedEmail Flag to indicate if the user's email is verified. + * @param bool|null $verifiedPhone Flag to indicate if the user's phone is verified. + * @param array|null $additionalLoginIds Additional login IDs for the user. + * @param array|null $ssoAppIds SSO app IDs associated with the user. + * @param UserPassword|null $password User's password information (cleartext or hashed). + * @return array The composed request body for updating user information. + */ public function composeUpdateBody( string $loginId, ?string $email, @@ -1339,55 +1534,33 @@ public function composeUpdateBody( ?string $familyName, ?array $roleNames, ?array $userTenants, - ?bool $test, ?string $picture, ?array $customAttributes, ?bool $verifiedEmail, ?bool $verifiedPhone, - ?array $additionalLoginIds, - ?array $ssoAppIds, - ?UserPassword $password + ?array $additionalIdentifiers, + ?array $ssoAppIds ): array { $res = [ 'loginId' => $loginId, 'email' => $email, - 'phone' => $phone, - 'displayName' => $displayName, - 'givenName' => $givenName, - 'middleName' => $middleName, - 'familyName' => $familyName, - 'roleNames' => $roleNames, - 'userTenants' => $userTenants, - 'test' => $test, - 'picture' => $picture, + 'phone' => $phone ?? '', + 'verifiedEmail' => $verifiedEmail ?? '', + 'verifiedPhone' => $verifiedPhone ?? '', + 'name' => $displayName ?? '', + 'roleNames' => $roleNames ?? [], + 'userTenants' => $userTenants ?? [], 'customAttributes' => $customAttributes ?? (object)[], - 'additionalLoginIds' => $additionalLoginIds, - 'ssoAppIds' => $ssoAppIds, + 'picture' => $picture ?? '', + 'additionalIdentifiers' => $additionalIdentifiers ?? [], + 'givenName' => $givenName ?? '', + 'middleName' => $middleName ?? '', + 'familyName' => $familyName ?? '', + 'ssoAppIds' => $ssoAppIds ?? [], ]; - - if ($verifiedEmail !== null) { - $res['verifiedEmail'] = $verifiedEmail; - } - if ($givenName !== null) { - $res['givenName'] = $givenName; - } - if ($middleName !== null) { - $res['middleName'] = $middleName; - } - if ($familyName !== null) { - $res['familyName'] = $familyName; - } - if ($verifiedPhone !== null) { - $res['verifiedPhone'] = $verifiedPhone; - } - if ($password !== null) { - if (isset($password->cleartext)) { - $res['password'] = $password->cleartext; - } else if (isset($password->hashed)) { - $res['hashedPassword'] = $password->hashed; - } - - return $res; - } + + return array_filter($res, function ($value) { + return $value !== null; + }); } } diff --git a/src/SDK/Management/UserPassword.php b/src/SDK/Management/UserPassword.php deleted file mode 100644 index c07c858..0000000 --- a/src/SDK/Management/UserPassword.php +++ /dev/null @@ -1,273 +0,0 @@ -hash = $hash; - $this->salt = $salt; - $this->iterations = $iterations; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - return [ - 'phpass' => [ - 'hash' => $this->hash, - 'salt' => $this->salt, - 'iterations' => $this->iterations - ], - ]; - } -} - -/** - * Class UserPasswordBcrypt - * - * Represents a user password hashed using the bcrypt algorithm. - */ -class UserPasswordBcrypt -{ - public string $hash; - - /** - * Constructor to initialize Bcrypt password details. - * - * @param string $hash The bcrypt hash in plaintext format (e.g., "$2a$..."). - */ - public function __construct(string $hash) - { - $this->hash = $hash; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - return [ - 'bcrypt' => [ - 'hash' => $this->hash, - ], - ]; - } -} - -/** - * Class UserPasswordFirebase - * - * Represents a user password hashed using Firebase's custom hashing scheme. - */ -class UserPasswordFirebase -{ - public string $hash; - public string $salt; - public string $saltSeparator; - public string $signerKey; - public int $memory; - public int $rounds; - - /** - * Constructor to initialize Firebase password details. - * - * @param string $hash Base64-encoded hash. - * @param string $salt Base64-encoded salt. - * @param string $saltSeparator Base64-encoded salt separator. - * @param string $signerKey Base64-encoded signer key. - * @param int $memory Memory cost (between 12 and 17). - * @param int $rounds Rounds cost (between 6 and 10). - */ - public function __construct( - string $hash, - string $salt, - string $saltSeparator, - string $signerKey, - int $memory, - int $rounds - ) { - $this->hash = $hash; - $this->salt = $salt; - $this->saltSeparator = $saltSeparator; - $this->signerKey = $signerKey; - $this->memory = $memory; - $this->rounds = $rounds; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - return [ - 'firebase' => [ - 'hash' => $this->hash, - 'salt' => $this->salt, - 'saltSeparator' => $this->saltSeparator, - 'signerKey' => $this->signerKey, - 'memory' => $this->memory, - 'rounds' => $this->rounds, - ], - ]; - } -} - - -/** - * Class UserPasswordPbkdf2 - * - * Represents a user password hashed using the PBKDF2 algorithm. - */ -class UserPasswordPbkdf2 -{ - public string $hash; - public string $salt; - public int $iterations; - public string $variant; - - /** - * Constructor to initialize PBKDF2 password details. - * - * @param string $hash Base64-encoded hash. - * @param string $salt Base64-encoded salt. - * @param int $iterations Number of iterations (usually in the thousands). - * @param string $variant Hash variant (sha1, sha256, or sha512). - */ - public function __construct( - string $hash, - string $salt, - int $iterations, - string $variant - ) { - $this->hash = $hash; - $this->salt = $salt; - $this->iterations = $iterations; - $this->variant = $variant; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - return [ - 'pbkdf2' => [ - 'hash' => $this->hash, - 'salt' => $this->salt, - 'iterations' => $this->iterations, - 'type' => $this->variant, - ], - ]; - } -} - -/** - * Class UserPasswordDjango - * - * Represents a user password hashed using Django's custom hashing scheme. - */ -class UserPasswordDjango -{ - public string $hash; - - /** - * Constructor to initialize Django password details. - * - * @param string $hash The django hash in plaintext format (e.g., "pbkdf2_sha256$..."). - */ - public function __construct(string $hash) - { - $this->hash = $hash; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - return [ - 'django' => [ - 'hash' => $this->hash, - ], - ]; - } -} - -/** - * Class UserPassword - * - * Represents either a cleartext password or a hashed password. - * This is used when creating or inviting users with a password. - */ -class UserPassword -{ - public ?string $cleartext; - public ?object $hashed; - - /** - * Constructor to initialize password details. - * Either cleartext or hashed password should be provided, not both. - * - * @param string|null $cleartext Plaintext password. - * @param object|null $hashed Hashed password object (one of the above classes). - */ - public function __construct(?string $cleartext = null, ?object $hashed = null) - { - $this->cleartext = $cleartext; - $this->hashed = $hashed; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - $data = []; - if ($this->cleartext !== null) { - $data['cleartext'] = $this->cleartext; - } - if ($this->hashed !== null) { - $data['hashed'] = $this->hashed->toArray(); - } - return $data; - } -} diff --git a/src/tests/Management/AuditTest.php b/src/tests/Management/AuditTest.php index 9711adc..960a721 100644 --- a/src/tests/Management/AuditTest.php +++ b/src/tests/Management/AuditTest.php @@ -3,106 +3,72 @@ namespace Descope\Tests\Management; use PHPUnit\Framework\TestCase; -use Descope\SDK\API; -use Descope\SDK\Management\Audit; -use Descope\SDK\Management\MgmtV1; +use Descope\SDK\DescopeSDK; class AuditTest extends TestCase { - private $apiMock; - private $audit; + private DescopeSDK $descopeSDK; protected function setUp(): void { - $this->apiMock = $this->createMock(API::class); - $this->audit = new Audit($this->apiMock); + $config = [ + 'projectId' => 'P2OkfVnJi5Ht7mpCqHjx17nV5epH', + 'managementKey' => 'K2o2rLwk3N3QI7kyJcRUmULKXqB7mKzpY7Dk6Hl24IXRM25YcYDYPFMKCO4SmUTDJJluxlu', + ]; + + $this->descopeSDK = new DescopeSDK($config); } - public function testSearch() + public function testSearchAudit() { - $response = [ - 'audits' => [ - [ - 'projectId' => 'project1', - 'userId' => 'user1', - 'action' => 'login', - 'occurred' => 1650000000000, - 'device' => 'mobile', - 'method' => 'otp', - 'geo' => 'US', - 'remoteAddress' => '192.168.1.1', - 'externalIds' => ['login1'], - 'tenants' => ['tenant1'], - 'data' => ['key' => 'value'] - ] - ] - ]; - - $this->apiMock - ->expects($this->once()) - ->method('doPost') - ->with(MgmtV1::AUDIT_SEARCH, $this->anything(), true) - ->willReturn($response); + $userIds = ['user1']; + $actions = ['login']; - $result = $this->audit->search(['user1'], ['login']); + // Perform the search + $result = $this->descopeSDK->management->audit->search($userIds, $actions); + // Assertions $this->assertIsArray($result); $this->assertArrayHasKey('audits', $result); - $this->assertCount(1, $result['audits']); - $this->assertEquals('project1', $result['audits'][0]['projectId']); - $this->assertEquals('user1', $result['audits'][0]['userId']); + $this->assertIsArray($result['audits']); + + foreach ($result['audits'] as $audit) { + $this->assertArrayHasKey('projectId', $audit); + $this->assertArrayHasKey('userId', $audit); + $this->assertArrayHasKey('action', $audit); + $this->assertArrayHasKey('occurred', $audit); + } } - public function testCreateEvent() + public function testCreateAuditEvent() { - $this->apiMock - ->expects($this->once()) - ->method('doPost') - ->with( - MgmtV1::AUDIT_CREATE_EVENT, - [ - 'action' => 'login', - 'type' => 'info', - 'actorId' => 'actor1', - 'tenantId' => 'tenant1', - 'userId' => 'user1', - 'data' => ['key' => 'value'] - ], - true - ); + // Create an audit event + $this->descopeSDK->management->audit->createEvent( + 'login', + 'info', + 'actor1', + 'tenant1', + 'user1', + ['key' => 'value'] + ); - $this->audit->createEvent('login', 'info', 'actor1', 'tenant1', 'user1', ['key' => 'value']); + // If no exceptions were thrown, the test passes. + $this->assertTrue(true); } - public function testConvertAuditRecord() + public function testCreateAuditEventWithoutUserId() { - $auditRecord = [ - 'projectId' => 'project1', - 'userId' => 'user1', - 'action' => 'login', - 'occurred' => 1650000000000, - 'device' => 'mobile', - 'method' => 'otp', - 'geo' => 'US', - 'remoteAddress' => '192.168.1.1', - 'externalIds' => ['login1'], - 'tenants' => ['tenant1'], - 'data' => ['key' => 'value'] - ]; + // Create an audit event without a userId + $this->descopeSDK->management->audit->createEvent( + 'login', + 'info', + 'actor1', + 'tenant1', + null, + ['key' => 'value'] + ); - $result = $this->audit->convertAuditRecord($auditRecord); - - $this->assertIsArray($result); - $this->assertEquals('project1', $result['projectId']); - $this->assertEquals('user1', $result['userId']); - $this->assertEquals('login', $result['action']); - $this->assertInstanceOf(DateTime::class, $result['occurred']); - $this->assertEquals('mobile', $result['device']); - $this->assertEquals('otp', $result['method']); - $this->assertEquals('US', $result['geo']); - $this->assertEquals('192.168.1.1', $result['remoteAddress']); - $this->assertEquals(['login1'], $result['loginIds']); - $this->assertEquals(['tenant1'], $result['tenants']); - $this->assertEquals(['key' => 'value'], $result['data']); + // If no exceptions were thrown, the test passes. + $this->assertTrue(true); } -} +} \ No newline at end of file diff --git a/src/tests/Management/UserPwdTest.php b/src/tests/Management/UserPwdTest.php index 821eb0e..ec6eeb6 100644 --- a/src/tests/Management/UserPwdTest.php +++ b/src/tests/Management/UserPwdTest.php @@ -3,13 +3,13 @@ namespace Descope\Tests\Management; use PHPUnit\Framework\TestCase; -use Descope\SDK\Management\UserPassword; -use Descope\SDK\Management\UserPasswordBcrypt; -use Descope\SDK\Management\UserPasswordFirebase; -use Descope\SDK\Management\UserPasswordPbkdf2; -use Descope\SDK\Management\UserPasswordDjango; +use Descope\SDK\Management\Password\UserPassword; +use Descope\SDK\Management\Password\UserPasswordBcrypt; +use Descope\SDK\Management\Password\UserPasswordFirebase; +use Descope\SDK\Management\Password\UserPasswordPbkdf2; +use Descope\SDK\Management\Password\UserPasswordDjango; -class UserPasswordTest extends TestCase +class UserPwdTest extends TestCase { public function testUserPasswordBcrypt() { diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index cad13c4..b140bea 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -4,22 +4,24 @@ use PHPUnit\Framework\TestCase; use Descope\SDK\DescopeSDK; -use Descope\SDK\Management\UserPassword; -use Descope\SDK\Management\UserPasswordBcrypt; -use Descope\SDK\Management\UserPasswordFirebase; -use Descope\SDK\Management\UserPasswordPbkdf2; -use Descope\SDK\Management\UserPasswordDjango; +use Descope\SDK\Management\Password\UserPassword; +use Descope\SDK\Management\Password\UserPasswordBcrypt; +use Descope\SDK\Management\Password\UserPasswordFirebase; +use Descope\SDK\Management\Password\UserPasswordPbkdf2; +use Descope\SDK\Management\Password\UserPasswordDjango; use Descope\SDK\Management\User; use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\UserObj; -use Descope\SDK\Auth\LoginOptions; +use Descope\SDK\Management\LoginOptions; use Descope\SDK\Common\DeliveryMethod; use Descope\SDK\Exception\AuthException; use GuzzleHttp\Exception\RequestException; -class UserPasswordTest extends TestCase +class UserTest extends TestCase { private DescopeSDK $descopeSDK; + private string $createdUserLoginId; + private string $createdUserId; protected function setUp(): void { @@ -48,12 +50,14 @@ public function testCreateUser() "http://example.com/invite", ["additionalLoginId1"], ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"], - new UserPassword("password123"), + new UserPassword("Password123!"), ["user"], - [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant Admin"])] + [new AssociatedTenant("T2o2zKibuWuCVH4lqJrSfFuXss06", ["Tenant Admin"])] ); + $this->createdUserLoginId = $response['user']['loginIds'][0] ?? null; + $this->createdUserId = $response['user']['userId'] ?? null; + $this->assertArrayHasKey('userId', $response); - print_r($response); } public function testCreateTestUser() @@ -73,12 +77,10 @@ public function testCreateTestUser() "http://example.com/invite2", ["additionalLoginId2"], ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"], - new UserPassword(cleartext: "password456"), - ["user"], - [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant User"])] + new UserPassword("Password456!"), + ["user"] ); $this->assertArrayHasKey('userId', $response); - print_r($response); } public function testInviteUser() @@ -100,12 +102,11 @@ public function testInviteUser() true, ["additionalLoginId3"], ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"], - new UserPassword(hashed: new UserPasswordBcrypt("$2y$10$/brZw23J/ya5sOJl8vm7H.BqhDnLqH4ohtSKcZYvSVP/hE6veK.0K")), + new UserPassword("", new UserPasswordBcrypt("$2y$10$/brZw23J/ya5sOJl8vm7H.BqhDnLqH4ohtSKcZYvSVP/hE6veK.0K")), ["user"], - [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant User"])] + [new AssociatedTenant("T2o2zKibuWuCVH4lqJrSfFuXss06", ["Tenant Admin"])] ); $this->assertArrayHasKey('userId', $response); - print_r($response); } public function testInviteBatchUsers() @@ -120,14 +121,14 @@ public function testInviteBatchUsers() "Middle", "User", ["user"], - [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant User"])], + [new AssociatedTenant("T2o2zKibuWuCVH4lqJrSfFuXss06", ["Tenant Admin"])], "http://example.com/picture1.jpg", - ["customAttr1" => "value1"], + [], true, true, ["additionalLoginId1"], [], - new UserPassword(cleartext: "password123") + new UserPassword("Password123!"), ), new UserObj( "batchuser2", @@ -138,72 +139,62 @@ public function testInviteBatchUsers() "Middle", "User", ["user"], - [new AssociatedTenant("T2SrweL5J2y8YOh8DyDbGpZXejBA", ["Tenant User"])], + [], "http://example.com/picture2.jpg", - ["customAttr2" => "value2"], + [], true, true, ["additionalLoginId2"], [], - new UserPassword(cleartext: "password456") + new UserPassword("Password456!"), ) ]; $response = $this->descopeSDK->management->user->inviteBatch($users, "http://example.com/invitebatch", true, true); - $this->assertArrayHasKey('userIds', $response); - print_r($response); + $this->assertArrayHasKey('createdUsers', $response); } public function testUpdateUser() { $this->descopeSDK->management->user->update( - "testuser1", + "use login id from previously created user", "newtestuser1@example.com", - "+14152464801", + "", "Updated Test User", - "Updated", - "Middle", - "User", + "", + "", + "", "http://example.com/newpicture.jpg", - ["dob" => "newvalue1"], - true, + [], true, - ["additionalLoginId1"], - ["SA2ZsUj73JFqUn8iQx9tblndjKCc6"] + false, + ["additionalLoginId1"] ); - $this->assertTrue(true); // Assert no exception thrown - } - - public function testDeleteUser() - { - $this->descopeSDK->management->user->delete("testuser1"); - $this->assertTrue(true); // Assert no exception thrown + print("Hello!"); + $this->assertTrue(true); } public function testLoadUser() { - $response = $this->descopeSDK->management->user->load("testuser1"); - $this->assertArrayHasKey('userId', $response); - print_r($response); + $response = $this->descopeSDK->management->user->load($this->createdUserLoginId); + $this->assertArrayHasKey('user', $response); } public function testLoadUserByUserId() { - $response = $this->descopeSDK->management->user->loadByUserId("U2goH2ldn4SzXoFm6IWKlRiEq6JV"); - $this->assertArrayHasKey('userId', $response); - print_r($response); + $response = $this->descopeSDK->management->user->loadByUserId($this->createdUserId); + $this->assertArrayHasKey('user', $response); } public function testGenerateEmbeddedLink() { - $response = $this->descopeSDK->management->user->generateEmbeddedLink("testuser1"); + $response = $this->descopeSDK->management->user->generateEmbeddedLink("kevin+1@descope.com"); $this->assertIsString($response); - print_r($response); } public function testGenerateEnchantedLinkForTestUser() { - $loginOptions = new LoginOptions(['stepup' => true, 'mfa' => true]); + $loginOptions = new LoginOptions(true, true); $response = $this->descopeSDK->management->user->generateEnchantedLinkForTestUser( "testuser1", "http://example.com/redirect", @@ -211,13 +202,6 @@ public function testGenerateEnchantedLinkForTestUser() ); $this->assertArrayHasKey('link', $response); $this->assertArrayHasKey('pendingRef', $response); - print_r($response); - } - - public function testLogoutUser() - { - $response = $this->descopeSDK->management->user->logout("testuser1"); - $this->assertTrue(true); // Assert no exception thrown } public function testSearchAllUsers() @@ -236,27 +220,29 @@ public function testSearchAllUsers() [] ); $this->assertArrayHasKey('users', $response); - print_r($response); - } - - public function testGetProviderToken() - { - $response = $this->descopeSDK->management->user->getProviderToken("testuser1", "google"); - $this->assertArrayHasKey('accessToken', $response); - print_r($response); } public function testActivateUser() { $response = $this->descopeSDK->management->user->activate("testuser1"); - $this->assertArrayHasKey('status', $response); - print_r($response); + $this->assertTrue(true); } public function testDeactivateUser() { $response = $this->descopeSDK->management->user->deactivate("testuser1"); - $this->assertArrayHasKey('status', $response); - print_r($response); + $this->assertTrue(true); + } + + public function testDeleteUser() + { + $this->descopeSDK->management->user->delete("testuser1"); + $this->assertTrue(true); + } + + public function testDeleteAllTestUsers() + { + $this->descopeSDK->management->user->deleteAllTestUsers(); + $this->assertTrue(true); } } From 1bea7a447395eb3817aec91a15a549a69035c8ed Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 22:05:28 -0700 Subject: [PATCH 12/13] reverted some breaking changes --- src/SDK/API.php | 2 +- src/tests/Management/UserTest.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index 717f669..abd435c 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -62,7 +62,7 @@ private function transformEmptyArraysToObjects($data) if (is_array($value)) { // If the array is empty, ensure it's preserved as an empty array if (empty($value)) { - $value = []; + $value = new \stdClass(); } else { // Recur for non-empty arrays $value = $this->transformEmptyArraysToObjects($value); diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index b140bea..19ecf65 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -170,7 +170,6 @@ public function testUpdateUser() false, ["additionalLoginId1"] ); - print("Hello!"); $this->assertTrue(true); } From 738af6fdfcc3953e8c31f5a7ca4f348e473572c2 Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Sun, 27 Oct 2024 22:06:29 -0700 Subject: [PATCH 13/13] fixed linter --- src/tests/Management/AuditTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/Management/AuditTest.php b/src/tests/Management/AuditTest.php index 960a721..75b310c 100644 --- a/src/tests/Management/AuditTest.php +++ b/src/tests/Management/AuditTest.php @@ -71,4 +71,4 @@ public function testCreateAuditEventWithoutUserId() // If no exceptions were thrown, the test passes. $this->assertTrue(true); } -} \ No newline at end of file +}