Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Easier integration with 3rd parties #44

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

xavierleune
Copy link

The main changes are:

  • Allow injection of the Storage service and uncouple the Storage facade and the concrete storage ; 2 implementations here: the cookie storage (but with jwks url not stored in cookies) and a naive session storage ; on my side I have a session storage integrated with symfony that works pretty well
  • Remove all static functions but getInstance on storage, as storage is a singleton we work in a non static context
  • Add getters for useful url for better integration with 3rd party frameworks
  • Fix some return types to work easier with the lib and to know what to expect

Copy link
Contributor

coderabbitai bot commented Nov 6, 2024

Walkthrough

The pull request includes modifications to several files related to the Kinde SDK. Key changes involve updating the .gitignore file to exclude the composer.lock file from version control. In the KindeClientSDK, property types have been adjusted for better type safety, and method signatures have been refined for clarity. The AuthorizationCode and ClientCredentials classes have seen method updates to improve flexibility and error handling. Additionally, new classes for cookie and session storage have been introduced, alongside a new StorageInterface, enhancing the storage management capabilities of the SDK.

Changes

File Change Summary
.gitignore Added composer.lock to the list of ignored files.
lib/KindeClientSDK.php Updated property types for redirectUri, logoutRedirectUri, and storage. Refined method signatures for several methods, including login, register, and fetchToken. Simplified control flow in the login method.
lib/Sdk/OAuth2/AuthorizationCode.php Added authenticate method and updated buildAuthorizationURI method to enhance flexibility and streamline parameter handling.
lib/Sdk/OAuth2/ClientCredentials.php Updated visibility and type for $storage property. Refined authenticate method's parameter type and return type for clarity. Simplified error handling in the authenticate method.
lib/Sdk/Storage/BaseStorage.php Removed static properties and transitioned methods from static to instance scope. Introduced `StorageInterface
lib/Sdk/Storage/CookieStorage.php Introduced CookieStorage class implementing StorageInterface, providing methods for cookie management with configurable options.
lib/Sdk/Storage/SessionStorage.php Added SessionStorage class implementing StorageInterface for session management, including methods for retrieving, setting, and removing session items.
lib/Sdk/Storage/Storage.php Transitioned several static methods to instance methods, enhancing encapsulation and allowing for multiple instances. Updated method signatures to reflect the new instance context.
lib/Sdk/Storage/StorageInterface.php Introduced StorageInterface defining methods for item management, establishing a contract for storage operations.

Possibly related PRs

  • security: Removed client token from store. #42: The changes in the ClientCredentials class regarding the removal of the token storage directly relate to the modifications in the KindeClientSDK class, which also involves the handling of storage and authentication processes.

Suggested reviewers

  • davidkinde

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 23

🧹 Outside diff range and nitpick comments (15)
lib/Sdk/Storage/StorageInterface.php (2)

5-6: Add PHPDoc documentation for the interface.

Since this is a public interface that will be implemented by third parties, it should be well-documented.

Add this documentation block:

+/**
+ * Interface for storage implementations in the Kinde SDK.
+ * 
+ * This interface defines the contract for storing and retrieving data
+ * in the Kinde SDK. Implementations can use different storage backends
+ * such as cookies, sessions, or other persistence mechanisms.
+ *
+ * @since 1.0.0
+ */
 interface StorageInterface

1-16: Consider enhancing storage abstraction for better third-party integration.

While this interface provides basic storage operations, consider these architectural improvements for better third-party integration:

  1. Add a factory interface for creating storage implementations
  2. Consider supporting async operations for future-proofing
  3. Add support for bulk operations
  4. Consider adding TTL (Time To Live) support separate from expires

This would make the SDK more flexible for various framework integrations and use cases.

lib/Sdk/Storage/SessionStorage.php (4)

1-6: Add class-level documentation.

Consider adding a PHPDoc block to describe the class purpose, implementation details, and usage examples.

 <?php

 namespace Kinde\KindeSDK\Sdk\Storage;

+/**
+ * Session-based implementation of StorageInterface.
+ * 
+ * This class provides session storage capabilities for the Kinde SDK,
+ * managing data persistence across requests while handling expiration.
+ */
 class SessionStorage implements StorageInterface

37-41: Add key validation in removeItem method.

While the implementation is functional, consider adding key validation for consistency with other methods.

     public function removeItem(string $key): void
     {
         $this->startSession();
+        if (empty($key)) {
+            throw new \InvalidArgumentException('Key cannot be empty');
+        }
         unset($_SESSION[$key]);
     }

43-48: Consider adding session security configurations.

While the session initialization works correctly, consider adding security-related configurations.

     private function startSession(): void
     {
         if (session_status() === PHP_SESSION_NONE) {
+            // Set secure session configurations
+            ini_set('session.cookie_httponly', '1');
+            ini_set('session.cookie_secure', '1');
+            ini_set('session.use_strict_mode', '1');
             session_start();
         }
     }

1-49: Consider environment-specific session handling configurations.

The current implementation might not be suitable for all environments (e.g., CLI, workers). Consider:

  1. Adding a configuration mechanism for session options
  2. Implementing environment checks
  3. Providing a fallback mechanism for environments where sessions aren't available

Would you like assistance in implementing these architectural improvements?

lib/Sdk/Storage/CookieStorage.php (2)

13-16: Add input validation and documentation for getItem method.

The method lacks input validation and documentation.

Consider these improvements:

+    /**
+     * Retrieve a value from cookie storage
+     * @param string $key Cookie key
+     * @return string Cookie value or empty string if not found
+     * @throws \InvalidArgumentException If key is empty
+     */
     public function getItem(string $key): string
     {
+        if (empty($key)) {
+            throw new \InvalidArgumentException('Cookie key cannot be empty');
+        }
         return $_COOKIE[self::getKey($key)] ?? "";
     }

42-45: Improve type safety and add validation for key generation.

The utility method lacks type hints and input validation.

Consider these improvements:

+    /**
+     * Generate prefixed cookie key
+     * @param string $key Base key
+     * @return string Prefixed key
+     */
-    private function getKey($key)
+    private function getKey(string $key): string
     {
+        $key = trim($key);
+        $key = preg_replace('/[^a-zA-Z0-9_-]/', '', $key);
         return self::$prefix . '_' . $key;
     }
lib/Sdk/OAuth2/ClientCredentials.php (1)

46-51: Consider enhancing error handling for HTTP responses

While JSON decoding errors are handled explicitly, HTTP errors from the Guzzle client could provide more context. Consider wrapping the request in a try-catch block with specific error messages.

-        $response =
-            $client->request('POST', $clientSDK->tokenEndpoint, [
-                'form_params' => $formData
-            ]);
-        $token = $response->getBody()->getContents();
-        return json_decode($token, false, 512, JSON_THROW_ON_ERROR);
+        try {
+            $response = $client->request('POST', $clientSDK->tokenEndpoint, [
+                'form_params' => $formData
+            ]);
+            $token = $response->getBody()->getContents();
+            return json_decode($token, false, 512, JSON_THROW_ON_ERROR);
+        } catch (\GuzzleHttp\Exception\ClientException $e) {
+            throw new \RuntimeException('Authentication failed: ' . $e->getResponse()->getBody()->getContents(), $e->getCode(), $e);
+        } catch (\GuzzleHttp\Exception\ServerException $e) {
+            throw new \RuntimeException('Authentication server error: ' . $e->getResponse()->getBody()->getContents(), $e->getCode(), $e);
+        }
lib/Sdk/Storage/Storage.php (3)

12-14: Add type declaration for tokenTimeToLive property.

For better type safety and consistency with jwksUrl, consider adding a type declaration for the tokenTimeToLive property.

-    private $tokenTimeToLive;
+    private int $tokenTimeToLive;

Line range hint 100-111: Add error handling for JWT parsing.

The getUserProfile method (and similar JWT parsing methods) should include error handling for invalid JWT tokens. Consider wrapping the JWT parsing in a try-catch block to handle potential parsing errors gracefully.

     public function getUserProfile()
     {
         $token = $this->getToken();
-        $payload = Utils::parseJWT($token['id_token'] ?? '');
-        return [
-            'id' => $payload['sub'] ?? '',
-            'given_name' => $payload['given_name'] ?? '',
-            'family_name' => $payload['family_name'] ?? '',
-            'email' => $payload['email'] ?? '',
-            'picture' => $payload['picture'] ?? '',
-        ];
+        try {
+            $payload = Utils::parseJWT($token['id_token'] ?? '');
+            return [
+                'id' => $payload['sub'] ?? '',
+                'given_name' => $payload['given_name'] ?? '',
+                'family_name' => $payload['family_name'] ?? '',
+                'email' => $payload['email'] ?? '',
+                'picture' => $payload['picture'] ?? '',
+            ];
+        } catch (\Exception $e) {
+            return [
+                'id' => '',
+                'given_name' => '',
+                'family_name' => '',
+                'email' => '',
+                'picture' => '',
+            ];
+        }
     }

24-34: Well-structured transition to instance methods.

The refactoring from static to instance methods while maintaining the singleton pattern through getInstance is well-implemented. This change:

  • Improves testability by allowing dependency injection
  • Maintains backward compatibility
  • Aligns with the PR objective of easier third-party integration

Consider documenting the singleton usage pattern in the class documentation to guide implementers on proper instantiation.

lib/Sdk/Storage/BaseStorage.php (1)

10-10: Consider restricting the visibility of $innerStorage.

The $innerStorage property is declared as public, which allows external modification and can lead to unintended side effects. It's recommended to change its visibility to protected or private to encapsulate the storage mechanism and prevent external interference.

lib/Sdk/OAuth2/AuthorizationCode.php (1)

37-44: Consider refactoring method parameters for buildAuthorizationURI

The buildAuthorizationURI method accepts multiple parameters. It might improve readability and maintainability to refactor these parameters into an associative array or an object. This approach can make the code more scalable and easier to manage, especially if more parameters are added in the future.

lib/KindeClientSDK.php (1)

Line range hint 586-588: Avoid suppressing exceptions without handling them in isAuthenticated

In the isAuthenticated method, the catch block is empty, which suppresses all exceptions without any handling. This can make debugging difficult and may hide underlying issues.

Consider logging the exception or handling it appropriately to ensure that any errors during the refresh token process are not silently ignored.

Apply this diff to log the exception:

 } catch (\Throwable $th) {
+    error_log('Exception during token refresh: ' . $th->getMessage());
 }

Alternatively, rethrow the exception or handle it as needed.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f39e348 and a492833.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock, !**/*.lock
📒 Files selected for processing (9)
  • .gitignore (1 hunks)
  • lib/KindeClientSDK.php (17 hunks)
  • lib/Sdk/OAuth2/AuthorizationCode.php (1 hunks)
  • lib/Sdk/OAuth2/ClientCredentials.php (2 hunks)
  • lib/Sdk/Storage/BaseStorage.php (2 hunks)
  • lib/Sdk/Storage/CookieStorage.php (1 hunks)
  • lib/Sdk/Storage/SessionStorage.php (1 hunks)
  • lib/Sdk/Storage/Storage.php (4 hunks)
  • lib/Sdk/Storage/StorageInterface.php (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🔇 Additional comments (4)
lib/Sdk/Storage/StorageInterface.php (1)

1-4: LGTM! Proper namespace and file structure.

The namespace follows PSR-4 autoloading standard and matches the directory structure.

lib/Sdk/Storage/CookieStorage.php (1)

1-46: Verify cookie handling across the codebase.

Let's ensure there are no conflicting cookie implementations or security issues.

✅ Verification successful

Cookie implementation is secure and properly isolated

The cookie implementation in CookieStorage.php is secure and well-implemented:

  • Security flags are properly set with secure defaults (httpOnly=true, secure=true, samesite=Lax)
  • No conflicting cookie implementations found in the codebase
  • Cookie storage is properly isolated through the StorageInterface
  • No direct cookie value echoing or XSS vulnerabilities detected
  • Test implementations use mock cookies (COOKIE_FAKE) instead of real ones
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other cookie implementations and security-sensitive patterns

echo "Checking for other cookie implementations..."
rg -l "setcookie|$_COOKIE" --type php

echo "Checking for security-sensitive patterns in cookie handling..."
rg "cookie.*secure.*=.*false|httponly.*=.*false" --type php

echo "Checking for potential XSS vulnerabilities in cookie handling..."
rg "echo.*\$_COOKIE" --type php

Length of output: 4010


Script:

#!/bin/bash
# Let's check for actual cookie implementations and storage classes

# Check for cookie-related implementations
echo "=== Checking cookie storage implementations ==="
rg -A 10 "class.*Storage.*implements StorageInterface" --type php

# Check for cookie-related configurations
echo "=== Checking cookie configurations ==="
rg "cookie.*=|COOKIE" --type php -A 2 -B 2

# Check session storage implementation for comparison
echo "=== Checking session storage implementation ==="
cat lib/Sdk/Storage/SessionStorage.php

Length of output: 7271

lib/Sdk/OAuth2/ClientCredentials.php (1)

17-17: LGTM: Type declaration enhances type safety

The addition of the Storage type declaration aligns with modern PHP practices and improves code maintainability.

lib/Sdk/Storage/Storage.php (1)

9-9: Verify BaseStorage implementation.

Since this class extends BaseStorage, we should verify that all required storage interface methods are properly implemented.

✅ Verification successful

Let me gather more information about the implementation details.


Let me check one more thing to see the actual Storage class implementation since we're reviewing Storage.php.


Storage implementation is properly structured

The verification shows a well-implemented storage hierarchy:

  • StorageInterface defines the required contract with getItem, setItem, and removeItem methods
  • CookieStorage implements StorageInterface with proper cookie handling
  • BaseStorage provides a base implementation with CookieStorage as the default inner storage
  • Storage extends BaseStorage and adds token-specific functionality
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check BaseStorage implementation and interface
# Look for BaseStorage class definition and any interfaces it implements
rg -A 5 "class BaseStorage"

# Look for interface definitions that might be related to storage
rg -A 5 "interface.*Storage"

Length of output: 1052


Script:

#!/bin/bash
# Check complete StorageInterface definition
rg -A 15 "interface StorageInterface"

# Check BaseStorage implementation details
rg -A 20 "class BaseStorage" lib/Sdk/Storage/BaseStorage.php

# Look for getItem and setItem implementations
ast-grep --pattern 'function getItem($_) { $$$ }'
ast-grep --pattern 'function setItem($_) { $$$ }'

Length of output: 1412


Script:

#!/bin/bash
# Check Storage class implementation
rg -A 20 "class Storage" lib/Sdk/Storage/Storage.php

# Also check CookieStorage implementation since it's used as innerStorage
rg -A 20 "class CookieStorage"

Length of output: 2018

Comment on lines +7 to +16
public function getItem(string $key): string;

public function setItem(
string $key,
string $value,
int $expires = 0,
): void;

public function removeItem(string $key): void;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Essential improvements needed for robust storage interface.

Several improvements are needed to make this interface more robust and developer-friendly:

  1. The getItem method doesn't handle non-existent keys. It should either:

    • Return ?string to handle missing items
    • Or throw a specific exception
  2. The setItem method needs parameter validation for the expiration time.

  3. Method documentation is missing.

Here's the suggested implementation:

+/**
+ * Retrieves an item from storage by key.
+ *
+ * @param string $key The key of the item to retrieve
+ * @return string|null The value of the item, or null if not found
+ * @throws StorageException If retrieval fails
+ */
-public function getItem(string $key): string;
+public function getItem(string $key): ?string;

+/**
+ * Stores an item in storage.
+ *
+ * @param string $key The key under which to store the value
+ * @param string $value The value to store
+ * @param int $expires The expiration time in seconds (0 for no expiration)
+ * @throws StorageException If storage fails
+ * @throws \InvalidArgumentException If expires is negative
+ */
 public function setItem(
     string $key,
     string $value,
     int $expires = 0,
 ): void;

+/**
+ * Removes an item from storage.
+ *
+ * @param string $key The key of the item to remove
+ * @throws StorageException If removal fails
+ */
 public function removeItem(string $key): void;

Additionally, consider adding:

  1. A custom StorageException class for storage-specific errors
  2. A method to check if an item exists: hasItem(string $key): bool
  3. A method to clear all items: clear(): void
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getItem(string $key): string;
public function setItem(
string $key,
string $value,
int $expires = 0,
): void;
public function removeItem(string $key): void;
}
/**
* Retrieves an item from storage by key.
*
* @param string $key The key of the item to retrieve
* @return string|null The value of the item, or null if not found
* @throws StorageException If retrieval fails
*/
public function getItem(string $key): ?string;
/**
* Stores an item in storage.
*
* @param string $key The key under which to store the value
* @param string $value The value to store
* @param int $expires The expiration time in seconds (0 for no expiration)
* @throws StorageException If storage fails
* @throws \InvalidArgumentException If expires is negative
*/
public function setItem(
string $key,
string $value,
int $expires = 0,
): void;
/**
* Removes an item from storage.
*
* @param string $key The key of the item to remove
* @throws StorageException If removal fails
*/
public function removeItem(string $key): void;
}

Comment on lines +25 to +35
public function setItem(string $key, string $value, int $expires = 0): void
{
$this->startSession();
$_SESSION[$key] =
json_encode([
'key' => $key,
'value' => $value,
'exp' => $expires,
]
);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential JSON encoding issues and validate expiration time.

The current implementation has several potential issues:

  1. Missing error handling for JSON encoding
  2. No validation of the expiration timestamp
  3. The string concatenation syntax appears incorrect

Apply this fix:

     public function setItem(string $key, string $value, int $expires = 0): void
     {
         $this->startSession();
-        $_SESSION[$key] = 
-            json_encode([
+        if ($expires < 0) {
+            throw new \InvalidArgumentException('Expiration time cannot be negative');
+        }
+
+        try {
+            $_SESSION[$key] = json_encode([
                 'key' => $key,
                 'value' => $value,
                 'exp' => $expires,
-            ]
-        );
+            ], JSON_THROW_ON_ERROR);
+        } catch (\JsonException $e) {
+            throw new \RuntimeException('Failed to encode storage data', 0, $e);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function setItem(string $key, string $value, int $expires = 0): void
{
$this->startSession();
$_SESSION[$key] =
json_encode([
'key' => $key,
'value' => $value,
'exp' => $expires,
]
);
}
public function setItem(string $key, string $value, int $expires = 0): void
{
$this->startSession();
if ($expires < 0) {
throw new \InvalidArgumentException('Expiration time cannot be negative');
}
try {
$_SESSION[$key] = json_encode([
'key' => $key,
'value' => $value,
'exp' => $expires,
], JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
throw new \RuntimeException('Failed to encode storage data', 0, $e);
}
}

Comment on lines +7 to +23
public function getItem(string $key): string
{
$this->startSession();
if (!isset($_SESSION[$key])) {
return '';
}
$rawData = $_SESSION[$key];
/**
* @var array{'exp': int, 'key': string, 'value': string} $data
*/
$data = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
if ($data['exp'] > 0 && $data['exp'] < time()) {
$this->removeItem($key);
return '';
}
return $data['value'];
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve error handling and type safety in getItem method.

Several improvements could enhance the robustness of this method:

  1. The JSON decoding could throw an exception that isn't caught
  2. The decoded data structure isn't validated before accessing its properties
  3. There's a potential race condition between the isset check and data usage

Consider this safer implementation:

     public function getItem(string $key): string
     {
         $this->startSession();
         if (!isset($_SESSION[$key])) {
             return '';
         }
         $rawData = $_SESSION[$key];
+        try {
             /**
-             * @var array{'exp': int, 'key': string, 'value': string} $data
+             * @var mixed $decoded
              */
-            $data = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
+            $decoded = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
+            
+            if (!is_array($decoded) ||
+                !isset($decoded['exp']) ||
+                !isset($decoded['key']) ||
+                !isset($decoded['value']) ||
+                !is_int($decoded['exp']) ||
+                !is_string($decoded['key']) ||
+                !is_string($decoded['value'])
+            ) {
+                $this->removeItem($key);
+                return '';
+            }
+            $data = $decoded;
+
             if ($data['exp'] > 0 && $data['exp'] < time()) {
                 $this->removeItem($key);
                 return '';
             }
             return $data['value'];
+        } catch (\JsonException $e) {
+            $this->removeItem($key);
+            return '';
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getItem(string $key): string
{
$this->startSession();
if (!isset($_SESSION[$key])) {
return '';
}
$rawData = $_SESSION[$key];
/**
* @var array{'exp': int, 'key': string, 'value': string} $data
*/
$data = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
if ($data['exp'] > 0 && $data['exp'] < time()) {
$this->removeItem($key);
return '';
}
return $data['value'];
}
public function getItem(string $key): string
{
$this->startSession();
if (!isset($_SESSION[$key])) {
return '';
}
$rawData = $_SESSION[$key];
try {
/**
* @var mixed $decoded
*/
$decoded = json_decode($rawData, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($decoded) ||
!isset($decoded['exp']) ||
!isset($decoded['key']) ||
!isset($decoded['value']) ||
!is_int($decoded['exp']) ||
!is_string($decoded['key']) ||
!is_string($decoded['value'])
) {
$this->removeItem($key);
return '';
}
$data = $decoded;
if ($data['exp'] > 0 && $data['exp'] < time()) {
$this->removeItem($key);
return '';
}
return $data['value'];
} catch (\JsonException $e) {
$this->removeItem($key);
return '';
}
}

Comment on lines +32 to +40
public function removeItem(string $key): void
{
$newKey = self::getKey($key);
if (isset($_COOKIE[$newKey])) {
unset($_COOKIE[$newKey]);
self::setItem($key, "", -1);
}
self::setItem($key, "", -1);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix redundant cookie removal and add validation.

The method has a redundant call to setItem and lacks input validation.

Apply these fixes:

+    /**
+     * Remove a cookie from storage
+     * @param string $key Cookie key
+     * @throws \InvalidArgumentException If key is empty
+     */
     public function removeItem(string $key): void
     {
+        if (empty($key)) {
+            throw new \InvalidArgumentException('Cookie key cannot be empty');
+        }
         $newKey = self::getKey($key);
         if (isset($_COOKIE[$newKey])) {
             unset($_COOKIE[$newKey]);
             self::setItem($key, "", -1);
         }
-        self::setItem($key, "", -1);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function removeItem(string $key): void
{
$newKey = self::getKey($key);
if (isset($_COOKIE[$newKey])) {
unset($_COOKIE[$newKey]);
self::setItem($key, "", -1);
}
self::setItem($key, "", -1);
}
/**
* Remove a cookie from storage
* @param string $key Cookie key
* @throws \InvalidArgumentException If key is empty
*/
public function removeItem(string $key): void
{
if (empty($key)) {
throw new \InvalidArgumentException('Cookie key cannot be empty');
}
$newKey = self::getKey($key);
if (isset($_COOKIE[$newKey])) {
unset($_COOKIE[$newKey]);
self::setItem($key, "", -1);
}
}

Comment on lines +18 to +30
public function setItem(string $key, string $value, int $expires = 0): void
{
$newKey = self::getKey($key);
$_COOKIE[$newKey] = $value;
setcookie($newKey, $value, [
'expires' => $expires,
'path' => self::$cookiePath,
'domain' => self::$cookieDomain,
'samesite' => 'Lax',
'secure' => self::$cookieSecure,
'httponly' => self::$cookieHttpOnly
]);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add validation and improve security for setItem method.

The method needs input validation and has potential security concerns.

Consider these security improvements:

+    /**
+     * Store a value in cookie storage
+     * @param string $key Cookie key
+     * @param string $value Cookie value
+     * @param int $expires Expiration timestamp
+     * @throws \InvalidArgumentException If key is empty
+     */
     public function setItem(string $key, string $value, int $expires = 0): void
     {
+        if (empty($key)) {
+            throw new \InvalidArgumentException('Cookie key cannot be empty');
+        }
+        if ($value === null) {
+            $value = '';
+        }
         $newKey = self::getKey($key);
-        $_COOKIE[$newKey] = $value;
+        $options = [
+            'expires' => $expires,
+            'path' => self::$cookiePath,
+            'domain' => self::$cookieDomain,
+            'samesite' => 'Lax',
+            'secure' => self::$cookieSecure,
+            'httponly' => self::$cookieHttpOnly
+        ];
-        setcookie($newKey, $value, [
-            'expires' => $expires,
-            'path' => self::$cookiePath,
-            'domain' => self::$cookieDomain,
-            'samesite' => 'Lax',
-            'secure' => self::$cookieSecure,
-            'httponly' => self::$cookieHttpOnly
-        ]);
+        if (setcookie($newKey, $value, $options)) {
+            $_COOKIE[$newKey] = $value;
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function setItem(string $key, string $value, int $expires = 0): void
{
$newKey = self::getKey($key);
$_COOKIE[$newKey] = $value;
setcookie($newKey, $value, [
'expires' => $expires,
'path' => self::$cookiePath,
'domain' => self::$cookieDomain,
'samesite' => 'Lax',
'secure' => self::$cookieSecure,
'httponly' => self::$cookieHttpOnly
]);
}
/**
* Store a value in cookie storage
* @param string $key Cookie key
* @param string $value Cookie value
* @param int $expires Expiration timestamp
* @throws \InvalidArgumentException If key is empty
*/
public function setItem(string $key, string $value, int $expires = 0): void
{
if (empty($key)) {
throw new \InvalidArgumentException('Cookie key cannot be empty');
}
if ($value === null) {
$value = '';
}
$newKey = self::getKey($key);
$options = [
'expires' => $expires,
'path' => self::$cookiePath,
'domain' => self::$cookieDomain,
'samesite' => 'Lax',
'secure' => self::$cookieSecure,
'httponly' => self::$cookieHttpOnly
];
if (setcookie($newKey, $value, $options)) {
$_COOKIE[$newKey] = $value;
}
}

Comment on lines +49 to +51
'client_id' => $clientId,
'grant_type' => GrantType::authorizationCode,
'redirect_uri' => $clientSDK->redirectUri,
'redirect_uri' => $redirectUri,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove 'grant_type' from the authorization URL parameters

In the OAuth 2.0 Authorization Code flow, the 'grant_type' parameter is not included in the authorization request URL. It is used when exchanging the authorization code for an access token at the token endpoint. Including it in the authorization URL might cause unexpected behavior or errors.

Apply this diff to remove the 'grant_type' parameter:

         $searchParams = [
             'client_id' => $clientId,
-            'grant_type' => GrantType::authorizationCode,
             'redirect_uri' => $redirectUri,
             'response_type' => 'code',

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +32 to +34
if (!headers_sent()) {
exit(header('Location: ' . $this->buildAuthorizationURI($clientSDK->authorizationEndpoint, $clientSDK->clientId, $clientSDK->redirectUri, $clientSDK->scopes, $additionalParameters, $clientSDK->additionalParameters)));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct the use of header() and handle cases where headers have been sent

The use of exit(header('Location: ...')) is incorrect because header() returns void, so passing it to exit() is ineffective. Instead, call header() and then exit(); separately.

Additionally, if headers_sent() returns true, the redirect will not occur, and no feedback is provided. Consider handling the else case to inform the user or handle the error appropriately.

Apply this diff to fix the issue:

         if (!headers_sent()) {
-            exit(header('Location: ' . $this->buildAuthorizationURI($clientSDK->authorizationEndpoint, $clientSDK->clientId, $clientSDK->redirectUri, $clientSDK->scopes, $additionalParameters, $clientSDK->additionalParameters)));
+            header('Location: ' . $this->buildAuthorizationURI($clientSDK->authorizationEndpoint, $clientSDK->clientId, $clientSDK->redirectUri, $clientSDK->scopes, $additionalParameters, $clientSDK->additionalParameters));
+            exit();
         } else {
+            // Handle the case when headers have already been sent
+            // For example, throw an exception or log an error
+            throw new \RuntimeException('Cannot redirect, headers already sent.');
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!headers_sent()) {
exit(header('Location: ' . $this->buildAuthorizationURI($clientSDK->authorizationEndpoint, $clientSDK->clientId, $clientSDK->redirectUri, $clientSDK->scopes, $additionalParameters, $clientSDK->additionalParameters)));
}
if (!headers_sent()) {
header('Location: ' . $this->buildAuthorizationURI($clientSDK->authorizationEndpoint, $clientSDK->clientId, $clientSDK->redirectUri, $clientSDK->scopes, $additionalParameters, $clientSDK->additionalParameters));
exit();
} else {
// Handle the case when headers have already been sent
// For example, throw an exception or log an error
throw new \RuntimeException('Cannot redirect, headers already sent.');
}

Comment on lines +161 to +164
* @return object{'access_token': string, 'expires_in': int, 'id_token': string, 'refresh_token': string, 'scope': string, 'token_type': string}|void The authentication result.
*
* @throws InvalidArgumentException When an incorrect grant_type is provided.
* @throws Throwable When an error occurs during authentication.
* @throws \Throwable When an error occurs during authentication.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Inconsistent return types in login method

The login method has inconsistent return types depending on the grant type. For clientCredentials, it returns an object, but for other grant types, it returns void. This inconsistency may lead to confusion and potential errors when using the method.

Consider updating the method to have a consistent return type or properly document the expected return types for each grant type.

Comment on lines +220 to +221
header('Location: ' . $this->getLogoutUrl());
exit();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid performing redirects inside the logout method

The logout method currently performs an HTTP redirect by calling header('Location: ' . $this->getLogoutUrl()); and then exit();. This practice can make unit testing difficult and may not align with best practices for handling HTTP responses within a library.

Consider refactoring the logout method to return the logout URL, allowing the caller to handle the redirection. Alternatively, provide a separate method for generating the logout URL and leave the responsibility of redirecting to the application layer.

Apply this diff to refactor the method:

 public function logout(): void
 {
     $this->cleanStorage();
-    header('Location: ' . $this->getLogoutUrl());
-    exit();
+    // Caller can use getLogoutUrl() to redirect the user
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
header('Location: ' . $this->getLogoutUrl());
exit();
$this->cleanStorage();
// Caller can use getLogoutUrl() to redirect the user

@@ -508,7 +520,7 @@ private function fetchToken($formParams)

$token = $response->getBody()->getContents();
$this->storage->setToken($token);
$tokenDecode = json_decode($token, false);
$tokenDecode = json_decode($token, false, 512, JSON_THROW_ON_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential exceptions from json_decode

The use of JSON_THROW_ON_ERROR in json_decode will cause a JsonException to be thrown on error, but this exception is not currently caught or handled.

Wrap the json_decode call in a try-catch block to handle potential JsonException and provide appropriate error handling.

Apply this diff to handle the exception:

 $this->storage->setToken($token);
- $tokenDecode = json_decode($token, false, 512, JSON_THROW_ON_ERROR);
+ try {
+     $tokenDecode = json_decode($token, false, 512, JSON_THROW_ON_ERROR);
+ } catch (\JsonException $e) {
+     throw new \Exception('Failed to decode token JSON: ' . $e->getMessage(), 0, $e);
+ }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (1)
lib/KindeClientSDK.php (1)

Line range hint 77-152: Consider using StorageInterface for better dependency injection.

While the addition of the Storage parameter is good, using the concrete Storage class and getInstance() fallback could make testing and extending the SDK harder.

Consider this change:

- public Storage $storage;
+ use Kinde\KindeSDK\Sdk\Storage\StorageInterface;
+ 
+ public StorageInterface $storage;

- public function __construct(
-     string $domain,
-     ?string $redirectUri,
-     string $clientId,
-     string $clientSecret,
-     string $grantType,
-     ?string $logoutRedirectUri,
-     string $scopes = 'openid profile email offline',
-     array $additionalParameters = [],
-     string $protocol = "",
-     ?Storage $storage = null
+ public function __construct(
+     string $domain,
+     ?string $redirectUri,
+     string $clientId,
+     string $clientSecret,
+     string $grantType,
+     ?string $logoutRedirectUri,
+     string $scopes = 'openid profile email offline',
+     array $additionalParameters = [],
+     string $protocol = "",
+     ?StorageInterface $storage = null
  ) {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a492833 and 535036c.

📒 Files selected for processing (1)
  • lib/KindeClientSDK.php (18 hunks)
🔇 Additional comments (2)
lib/KindeClientSDK.php (2)

Line range hint 27-75: LGTM! Property changes enhance type safety and flexibility.

The changes to property types, particularly making URIs nullable and adding proper type hints, improve the SDK's flexibility and type safety.


159-182: LGTM! Improved login method with better type safety.

The switch statement is cleaner than the previous try-catch, and the return type documentation is more precise.

Comment on lines +224 to +229
public function getLogoutUrl(): string
{
$searchParams = [
'redirect' => $this->logoutRedirectUri
];
header('Location: ' . $this->logoutEndpoint . '?' . http_build_query($searchParams));
exit();
return $this->logoutEndpoint . '?' . http_build_query($searchParams);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure proper URL encoding of the redirect parameter.

The logout URL generation should encode the redirect parameter to prevent potential security issues.

Apply this change:

  public function getLogoutUrl(): string
  {
      $searchParams = [
-         'redirect' => $this->logoutRedirectUri
+         'redirect' => urlencode($this->logoutRedirectUri)
      ];
      return $this->logoutEndpoint . '?' . http_build_query($searchParams);
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function getLogoutUrl(): string
{
$searchParams = [
'redirect' => $this->logoutRedirectUri
];
header('Location: ' . $this->logoutEndpoint . '?' . http_build_query($searchParams));
exit();
return $this->logoutEndpoint . '?' . http_build_query($searchParams);
public function getLogoutUrl(): string
{
$searchParams = [
'redirect' => urlencode($this->logoutRedirectUri)
];
return $this->logoutEndpoint . '?' . http_build_query($searchParams);

Comment on lines +567 to +578
public function refreshWithToken(string $refreshToken)
{
$formParams = [
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
];

$token = $this->fetchToken($formParams);
return $token;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add return type hint and error handling to refreshWithToken method.

The new method needs proper type hinting and should handle potential errors during token refresh.

Apply these improvements:

- public function refreshWithToken(string $refreshToken)
+ /**
+  * Refreshes the access token using a refresh token.
+  *
+  * @param string $refreshToken The refresh token to use
+  * @return object{'access_token': string, 'expires_in': int, 'id_token': string, 'refresh_token': string, 'scope': string, 'token_type': string}
+  * @throws GuzzleException When the HTTP request fails
+  * @throws JsonException When token decoding fails
+  */
+ public function refreshWithToken(string $refreshToken): object
  {
+     if (empty($refreshToken)) {
+         throw new InvalidArgumentException('Refresh token cannot be empty');
+     }
      $formParams = [
          'client_id' => $this->clientId,
          'client_secret' => $this->clientSecret,
          'grant_type' => 'refresh_token',
          'refresh_token' => $refreshToken
      ];

      $token = $this->fetchToken($formParams);
      return $token;
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function refreshWithToken(string $refreshToken)
{
$formParams = [
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
];
$token = $this->fetchToken($formParams);
return $token;
}
/**
* Refreshes the access token using a refresh token.
*
* @param string $refreshToken The refresh token to use
* @return object{'access_token': string, 'expires_in': int, 'id_token': string, 'refresh_token': string, 'scope': string, 'token_type': string}
* @throws GuzzleException When the HTTP request fails
* @throws JsonException When token decoding fails
*/
public function refreshWithToken(string $refreshToken): object
{
if (empty($refreshToken)) {
throw new InvalidArgumentException('Refresh token cannot be empty');
}
$formParams = [
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken
];
$token = $this->fetchToken($formParams);
return $token;
}

@davidkinde davidkinde mentioned this pull request Nov 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant