Skip to content

Commit

Permalink
Configuration validator improvements (#657)
Browse files Browse the repository at this point in the history
* Configuration validator improvements

* Adjust type in cookieSecure state check
  • Loading branch information
evansims authored Oct 20, 2022
1 parent e35111c commit 9333bea
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 216 deletions.
346 changes: 171 additions & 175 deletions src/Configuration/SdkConfiguration.php

Large diffs are not rendered by default.

63 changes: 61 additions & 2 deletions src/Configuration/SdkState.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,19 @@ public function __construct(
public ?array $user = null,
public ?int $accessTokenExpiration = null
) {
$configuration = $configuration ?? [];
$this->applyConfigurationState($configuration);
if (null !== $configuration && [] !== $configuration) {
$this->applyConfiguration($configuration);
}

$this->validateProperties();
}

public function setIdToken(?string $idToken = null): self
{
if (null !== $idToken && '' === trim($idToken)) {
$idToken = null;
}

$this->idToken = $idToken;
return $this;
}
Expand All @@ -54,6 +61,10 @@ public function hasIdToken(): bool

public function setAccessToken(?string $accessToken = null): self
{
if (null !== $accessToken && '' === trim($accessToken)) {
$accessToken = null;
}

$this->accessToken = $accessToken;
return $this;
}
Expand All @@ -74,6 +85,10 @@ public function hasAccessToken(): bool
*/
public function setAccessTokenScope(?array $accessTokenScope): self
{
if (null !== $accessTokenScope && [] === $accessTokenScope) {
$accessTokenScope = null;
}

$this->accessTokenScope = $this->filterArray($accessTokenScope);
return $this;
}
Expand Down Expand Up @@ -110,6 +125,10 @@ public function pushAccessTokenScope(array|string $scopes): ?array

public function setRefreshToken(?string $refreshToken = null): self
{
if (null !== $refreshToken && '' === trim($refreshToken)) {
$refreshToken = null;
}

$this->refreshToken = $refreshToken;
return $this;
}
Expand All @@ -131,6 +150,10 @@ public function hasRefreshToken(): bool
*/
public function setUser(?array $user): self
{
if (null !== $user && [] === $user) {
$user = null;
}

$this->user = $user;
return $this;
}
Expand All @@ -151,6 +174,10 @@ public function hasUser(): bool

public function setAccessTokenExpiration(?int $accessTokenExpiration = null): self
{
if (null !== $accessTokenExpiration && $accessTokenExpiration < 0) {
$accessTokenExpiration = null;
}

$this->accessTokenExpiration = $accessTokenExpiration;
return $this;
}
Expand All @@ -165,4 +192,36 @@ public function hasAccessTokenExpiration(): bool
{
return null !== $this->accessTokenExpiration;
}

/**
* @return array<callable>
*
* @psalm-suppress MissingClosureParamType
*/
private function getPropertyValidators(): array
{
return [
'idToken' => fn ($value) => is_string($value) || null === $value,
'accessToken' => fn ($value) => is_string($value) || null === $value,
'accessTokenScope' => fn ($value) => is_array($value) || null === $value,
'refreshToken' => fn ($value) => is_string($value) || null === $value,
'user' => fn ($value) => is_array($value) || null === $value,
'accessTokenExpiration' => fn ($value) => is_int($value) || null === $value,
];
}

/**
* @return array<mixed>
*/
private function getPropertyDefaults(): array
{
return [
'idToken' => null,
'accessToken' => null,
'accessTokenScope' => null,
'refreshToken' => null,
'user' => null,
'accessTokenExpiration' => null,
];
}
}
73 changes: 60 additions & 13 deletions src/Mixins/ConfigurableMixin.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,81 @@ trait ConfigurableMixin
*
* @psalm-suppress MissingClosureParamType,MissingClosureReturnType
*/
private function applyConfigurationState(?array $configuration): self
private function applyConfiguration(?array $configuration): self
{
if (null === $configuration) {
return $this;
}

foreach ($configuration as $configurationKey => $configurationValue) {
if (property_exists($this, $configurationKey)) {
$method = 'set' . ucfirst($configurationKey);
$validators = $this->getPropertyValidators();
$defaults = $this->getPropertyDefaults();

if (method_exists($this, $method)) {
$callback = function ($configurationValue) use ($method) {
// @phpstan-ignore-next-line
return $this->$method($configurationValue);
};
foreach ($configuration as $configKey => $configuredValue) {
if (! property_exists($this, $configKey) || ! array_key_exists($configKey, $defaults)) {
continue;
}

call_user_func($callback, $configurationValue);
continue;
}
if (! isset($validators[$configKey]) || ! is_callable($validators[$configKey])) {
throw \Auth0\SDK\Exception\ConfigurationException::validationFailed($configKey);
}

if ($validators[$configKey]($configuredValue) === false) {
throw \Auth0\SDK\Exception\ConfigurationException::validationFailed($configKey);
}

$method = 'set' . ucfirst($configKey);

if (method_exists($this, $method)) {
// @phpstan-ignore-next-line
$this->$configurationKey = $configurationValue;
$callback = function ($configuredValue) use ($method) {
// @phpstan-ignore-next-line
return $this->$method($configuredValue);
};

call_user_func($callback, $configuredValue);
continue;
}

// @phpstan-ignore-next-line
$this->$configKey = $configuredValue;
}

return $this;
}

/**
* @psalm-suppress MissingClosureParamType,MissingClosureReturnType
*/
private function validateProperties(): void
{
$defaults = $this->getPropertyDefaults();

foreach ($defaults as $configKey => $defaultValue) {
if (! property_exists($this, $configKey)) {
continue;
}

// @phpstan-ignore-next-line
if ($this->$configKey === $defaultValue) {
continue;
}

$method = 'set' . ucfirst($configKey);

if (method_exists($this, $method)) {
// @phpstan-ignore-next-line
$callback = function ($value) use ($method) {
// @phpstan-ignore-next-line
return $this->$method($value);
};

// @phpstan-ignore-next-line
call_user_func($callback, $this->$configKey);
continue;
}
}
}

/**
* @param mixed $value A value to compare against NULL.
* @param null|Throwable $throwable Optional. A Throwable exception to raise if $value is NULL.
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/API/AuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

test('__construct() accepts a configuration as an array', function(): void {
$auth = new Authentication([
'strategy' => 'api',
'strategy' => SdkConfiguration::STRATEGY_API,
'domain' => MockDomain::valid(),
'audience' => [uniqid()]
]);
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/API/Management/ManagementEndpointTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

beforeEach(function(): void {
$this->configuration = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'domain' => MockDomain::valid()
]);

Expand Down
26 changes: 11 additions & 15 deletions tests/Unit/Configuration/SdkConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
test('__construct() does not accept invalid types from configuration array', function(): void
{
$config = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'domain' => MockDomain::invalid(),
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, sprintf(\Auth0\SDK\Exception\ConfigurationException::MSG_VALIDATION_FAILED, 'domain'));
Expand Down Expand Up @@ -74,7 +74,7 @@
'clientId' => $clientId,
'redirectUri' => $redirectUri,
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_DOMAIN);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, sprintf(\Auth0\SDK\Exception\ConfigurationException::MSG_VALIDATION_FAILED, 'domain'));

test('__construct() throws an exception if domain is an invalid uri', function(): void {
$cookieSecret = uniqid();
Expand Down Expand Up @@ -143,7 +143,7 @@
'redirectUri' => $redirectUri,
'tokenLeeway' => 'TEST'
]);
})->throws(\TypeError::class);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, sprintf(\Auth0\SDK\Exception\ConfigurationException::MSG_VALIDATION_FAILED, 'tokenLeeway'));

test('successfully updates values', function(): void
{
Expand Down Expand Up @@ -192,7 +192,7 @@
test('a non-existent array value is ignored', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'domain' => MockDomain::valid(),
'clientId' => uniqid(),
'organization' => [],
Expand All @@ -214,23 +214,19 @@

test('a `webapp` strategy requires a domain', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'webapp',
]);
$sdk = new SdkConfiguration();
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_DOMAIN);

test('a `webapp` strategy requires a client id', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'webapp',
'domain' => MockDomain::valid()
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_CLIENT_ID);

test('a `webapp` strategy requires a client secret when HS256 is used', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'webapp',
'domain' => MockDomain::valid(),
'clientId' => uniqid(),
'tokenAlgorithm' => 'HS256'
Expand All @@ -240,37 +236,37 @@
test('a `api` strategy requires a domain', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'api',
'strategy' => SdkConfiguration::STRATEGY_API,
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_DOMAIN);

test('a `api` strategy requires an audience', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'api',
'strategy' => SdkConfiguration::STRATEGY_API,
'domain' => MockDomain::valid()
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_AUDIENCE);

test('a `management` strategy requires a domain', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'management'
'strategy' => SdkConfiguration::STRATEGY_MANAGEMENT_API
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_DOMAIN);

test('a `management` strategy requires a client id if a management token is not provided', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'management',
'strategy' => SdkConfiguration::STRATEGY_MANAGEMENT_API,
'domain' => MockDomain::valid()
]);
})->throws(\Auth0\SDK\Exception\ConfigurationException::class, \Auth0\SDK\Exception\ConfigurationException::MSG_REQUIRES_CLIENT_ID);

test('a `management` strategy requires a client secret if a management token is not provided', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'management',
'strategy' => SdkConfiguration::STRATEGY_MANAGEMENT_API,
'domain' => MockDomain::valid(),
'clientId' => uniqid()
]);
Expand All @@ -279,7 +275,7 @@
test('a `management` strategy does not require a client id or secret if a management token is provided', function(): void
{
$sdk = new SdkConfiguration([
'strategy' => 'management',
'strategy' => SdkConfiguration::STRATEGY_MANAGEMENT_API,
'domain' => MockDomain::valid(),
'managementToken' => uniqid()
]);
Expand Down
6 changes: 3 additions & 3 deletions tests/Unit/Store/CookieStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
$this->cookieSecret = uniqid() . bin2hex(random_bytes(32));

$this->configuration = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'cookieSecret' => $this->cookieSecret
]);

Expand Down Expand Up @@ -158,7 +158,7 @@

test('encrypt() throws an exception if a cookie secret is not configured', function(): void {
$this->configuration = new SdkConfiguration([
'strategy' => 'none'
'strategy' => SdkConfiguration::STRATEGY_NONE,
]);

$this->store = new CookieStore($this->configuration, $this->namespace);
Expand All @@ -168,7 +168,7 @@

test('decrypt() throws an exception if a cookie secret is not configured', function(array $state): void {
$this->configuration = new SdkConfiguration([
'strategy' => 'none'
'strategy' => SdkConfiguration::STRATEGY_NONE,
]);

$this->store = new CookieStore($this->configuration, $this->namespace);
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Store/Psr14StoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
$this->listener = new MockPsr14StoreListener();

$this->configuration = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'eventListenerProvider' => $this->listener->setup()
]);
});
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/Token/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
$this->cache = new ArrayAdapter();

$this->configuration = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'tokenCache' => $this->cache
]);
});
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/TokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
$this->cache = new ArrayAdapter();

$this->configuration = new SdkConfiguration([
'strategy' => 'none',
'strategy' => SdkConfiguration::STRATEGY_NONE,
'tokenCache' => $this->cache
]);
});
Expand Down
Loading

0 comments on commit 9333bea

Please sign in to comment.