Skip to content
Open
2 changes: 1 addition & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
parameters:
level: 7
level: 9
paths:
- ./src
- ./test
27 changes: 22 additions & 5 deletions src/PluginSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public function __construct(
// delete the instance if the special sub is in the token data
// exits the request
if ($sso && $remoteCallHandler && $sso->isDeleteInstanceCall()) {
$this->deleteInstance($sso->getInstanceId(), $remoteCallHandler);
$instanceId = $sso->getInstanceId() ?: throw new SSOException('Instance id is required for deleteInstance');
$this->deleteInstance($instanceId, $remoteCallHandler);
}

// starts the session
Expand Down Expand Up @@ -151,12 +152,28 @@ private function updateSSOInformation(string $jwt, string $appSecret, int $leewa
*/
private function validateParams(): ?string
{
$pid = $_REQUEST[self::QUERY_PARAM_PID] ?? null;
$jwt = $_REQUEST[self::QUERY_PARAM_JWT] ?? null;
$sid = $_REQUEST[self::QUERY_PARAM_SID] ?? null;
$rawPid = $_REQUEST[self::QUERY_PARAM_PID] ?? null;
$rawJwt = $_REQUEST[self::QUERY_PARAM_JWT] ?? null;
$rawSid = $_REQUEST[self::QUERY_PARAM_SID] ?? null;

// Normalize values to string|null while avoiding casting arrays/objects to string
$pid = null;
if (is_string($rawPid)) {
$pid = $rawPid;
}

$jwt = null;
if (is_string($rawJwt)) {
$jwt = $rawJwt;
}

$sid = null;
if (is_string($rawSid)) {
$sid = $rawSid;
}

// lets hint to bad class usage, as these cases should never happen.
if ($pid && $jwt) {
if ($pid !== null && $jwt !== null) {
throw new SSOAuthenticationException('Tried to initialize the session with both PID and JWT provided.');
}

Expand Down
10 changes: 9 additions & 1 deletion src/RemoteCall/DeleteInstanceTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ trait DeleteInstanceTrait
*
* if a remote call was not handled by the user we die hard here
*/
protected function exitRemoteCall(): void
protected function exitRemoteCall(): never
{
error_log("Warning: The exit procedure for a remote call was not properly handled.");
exit;
}

/**
* Handle the delete-instance remote call and terminate the request.
*
* @param string $instanceId
* @param RemoteCallInterface $remoteCallHandler
*
* @return never
*/
private function deleteInstance(string $instanceId, RemoteCallInterface $remoteCallHandler): void
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

deleteInstance() is documented as @return never and it always terminates via exitSuccess/exitFailure followed by exitRemoteCall(), but its signature is still : void. This mismatch makes the control-flow contract unclear for static analysis and readers. Consider changing the return type to never (or, if it can return in some scenarios, adjust the docblock accordingly).

Suggested change
private function deleteInstance(string $instanceId, RemoteCallInterface $remoteCallHandler): void
private function deleteInstance(string $instanceId, RemoteCallInterface $remoteCallHandler): never

Copilot uses AI. Check for mistakes.
{
if ($remoteCallHandler instanceof DeleteInstanceCallHandlerInterface) {
Expand Down
6 changes: 4 additions & 2 deletions src/SSOData/ClaimAccessTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ abstract protected function getAllClaims(): array;
*/
protected function getClaimSafe(string $name)
{

if ($this->hasClaim($name)) {
return $this->getClaim($name);
$value = $this->getClaim($name);

// Return the value as-is. Type safety is handled by individual getters.
return $value;
}

return null;
Expand Down
53 changes: 35 additions & 18 deletions src/SSOData/SSODataTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ trait SSODataTrait
*/
public function getBranchId(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_BRANCH_ID);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_BRANCH_ID);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -40,7 +41,8 @@ public function getBranchId(): ?string
*/
public function getBranchSlug(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_BRANCH_SLUG);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_BRANCH_SLUG);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -50,7 +52,8 @@ public function getBranchSlug(): ?string
*/
public function getSessionId(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_SESSION_ID);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_SESSION_ID);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -62,7 +65,8 @@ public function getSessionId(): ?string
*/
public function getInstanceId(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_INSTANCE_ID);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_INSTANCE_ID);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -72,7 +76,8 @@ public function getInstanceId(): ?string
*/
public function getInstanceName(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_INSTANCE_NAME);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_INSTANCE_NAME);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -82,7 +87,8 @@ public function getInstanceName(): ?string
*/
public function getUserId(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_ID);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_ID);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -95,7 +101,8 @@ public function getUserId(): ?string
*/
public function getUserExternalId(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_EXTERNAL_ID);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_EXTERNAL_ID);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -105,7 +112,8 @@ public function getUserExternalId(): ?string
*/
public function getUserUsername(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_USERNAME);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_USERNAME);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -115,7 +123,8 @@ public function getUserUsername(): ?string
*/
public function getUserPrimaryEmailAddress(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_PRIMARY_EMAIL_ADDRESS);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_PRIMARY_EMAIL_ADDRESS);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -125,7 +134,8 @@ public function getUserPrimaryEmailAddress(): ?string
*/
public function getFullName(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_FULL_NAME);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_FULL_NAME);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -135,7 +145,8 @@ public function getFullName(): ?string
*/
public function getFirstName(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_FIRST_NAME);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_FIRST_NAME);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -145,20 +156,22 @@ public function getFirstName(): ?string
*/
public function getLastName(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LAST_NAME);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LAST_NAME);
return is_string($value) ? $value : null;
}


/**
* Get the type of the token.
*
* The type of the accessing entity can be either a user or a token.
* The type of the accessing entity can be either a "user" or a "token".
*
* @return null|string
*/
public function getType(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_ENTITY_TYPE);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_ENTITY_TYPE);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -170,7 +183,8 @@ public function getType(): ?string
*/
public function getThemeTextColor(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_THEME_TEXT_COLOR);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_THEME_TEXT_COLOR);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -182,7 +196,8 @@ public function getThemeTextColor(): ?string
*/
public function getThemeBackgroundColor(): ?string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_THEME_BACKGROUND_COLOR);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_THEME_BACKGROUND_COLOR);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -192,7 +207,8 @@ public function getThemeBackgroundColor(): ?string
*/
public function getLocale(): string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LOCALE);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LOCALE);
return is_string($value) ? $value : '';
}

/**
Expand All @@ -202,6 +218,7 @@ public function getLocale(): string
*/
public function getTags(): ?array
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_TAGS);
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_TAGS);
return is_array($value) ? $value : null;
}
}
25 changes: 16 additions & 9 deletions src/SSOData/SharedDataTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public function getAudience(): ?string
*/
public function getExpireAtTime(): ?DateTimeImmutable
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_EXPIRE_AT);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_EXPIRE_AT);
return $value instanceof DateTimeImmutable ? $value : null;
}

/**
Expand All @@ -67,7 +68,8 @@ public function getExpireAtTime(): ?DateTimeImmutable
*/
public function getNotBeforeTime(): ?DateTimeImmutable
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_NOT_BEFORE);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_NOT_BEFORE);
return $value instanceof DateTimeImmutable ? $value : null;
}

/**
Expand All @@ -77,7 +79,8 @@ public function getNotBeforeTime(): ?DateTimeImmutable
*/
public function getIssuedAtTime(): ?DateTimeImmutable
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_ISSUED_AT);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_ISSUED_AT);
return $value instanceof DateTimeImmutable ? $value : null;
}

/**
Expand All @@ -87,7 +90,8 @@ public function getIssuedAtTime(): ?DateTimeImmutable
*/
public function getIssuer(): ?string
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_ISSUER);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_ISSUER);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -97,7 +101,8 @@ public function getIssuer(): ?string
*/
public function getId(): ?string
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_JWT_ID);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_JWT_ID);
return is_string($value) ? $value : null;
}

/**
Expand All @@ -107,21 +112,23 @@ public function getId(): ?string
*/
public function getSubject(): ?string
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_SUBJECT);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_SUBJECT);
return is_string($value) ? $value : null;
}

/**
* Get the role of the accessing user.
*
* If this is set to editor, the requesting user may manage the contents
* If this is set to "editor", the requesting user may manage the contents
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Image

* of the plugin instance, i.e. she has administration rights.
* The type of the accessing entity can be either a user or a “editor.
* The type of the accessing entity can be either a "user" or an "editor".
*
* @return null|string
*/
public function getRole(): ?string
{
return $this->getClaimSafe(SharedClaimsInterface::CLAIM_USER_ROLE);
$value = $this->getClaimSafe(SharedClaimsInterface::CLAIM_USER_ROLE);
return is_string($value) ? $value : null;
}

/**
Expand Down
44 changes: 37 additions & 7 deletions src/SSOTokenGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,52 @@ public static function createSignedTokenFromData(string $privateKey, array $toke
private static function buildToken(Configuration $config, array $tokenData): Token
{
$builder = $config->builder();
// Validate and coerce required registered claims to the expected types
$audience = $tokenData[SSOData\SharedClaimsInterface::CLAIM_AUDIENCE] ?? '';
if (!is_string($audience) || $audience === '') {
throw new \InvalidArgumentException('aud claim must be a non-empty string for token generation');
}

$issuedAt = $tokenData[SSOData\SharedClaimsInterface::CLAIM_ISSUED_AT] ?? null;
if (!($issuedAt instanceof \DateTimeImmutable)) {
throw new \InvalidArgumentException('iat claim must be a DateTimeImmutable for token generation');
}

$notBefore = $tokenData[SSOData\SharedClaimsInterface::CLAIM_NOT_BEFORE] ?? null;
if (!($notBefore instanceof \DateTimeImmutable)) {
throw new \InvalidArgumentException('nbf claim must be a DateTimeImmutable for token generation');
}

$expiresAt = $tokenData[SSOData\SharedClaimsInterface::CLAIM_EXPIRE_AT] ?? null;
if (!($expiresAt instanceof \DateTimeImmutable)) {
throw new \InvalidArgumentException('exp claim must be a DateTimeImmutable for token generation');
}

$token = $builder
->permittedFor($tokenData[SSOData\SharedClaimsInterface::CLAIM_AUDIENCE])
->issuedAt($tokenData[SSOData\SharedClaimsInterface::CLAIM_ISSUED_AT])
->canOnlyBeUsedAfter($tokenData[SSOData\SharedClaimsInterface::CLAIM_NOT_BEFORE])
->expiresAt($tokenData[SSOData\SharedClaimsInterface::CLAIM_EXPIRE_AT]);
->permittedFor($audience)
->issuedAt($issuedAt)
->canOnlyBeUsedAfter($notBefore)
->expiresAt($expiresAt);

if (isset($tokenData[SSOData\SharedClaimsInterface::CLAIM_ISSUER])) {
$token = $token->issuedBy($tokenData[SSOData\SharedClaimsInterface::CLAIM_ISSUER]);
$issuer = $tokenData[SSOData\SharedClaimsInterface::CLAIM_ISSUER];
if (is_string($issuer) && $issuer !== '') {
$token = $token->issuedBy($issuer);
}
}

if (isset($tokenData[SSOData\SSODataClaimsInterface::CLAIM_USER_ID])) {
$token = $token->relatedTo($tokenData[SSOData\SSODataClaimsInterface::CLAIM_USER_ID]);
$subject = $tokenData[SSOData\SSODataClaimsInterface::CLAIM_USER_ID];
if (is_string($subject) && $subject !== '') {
$token = $token->relatedTo($subject);
}
}

if (isset($tokenData[SSOData\SharedClaimsInterface::CLAIM_JWT_ID])) {
$token = $token->identifiedBy($tokenData[SSOData\SharedClaimsInterface::CLAIM_JWT_ID]);
$jwtId = $tokenData[SSOData\SharedClaimsInterface::CLAIM_JWT_ID];
if (is_string($jwtId) && $jwtId !== '') {
$token = $token->identifiedBy($jwtId);
}
}

// Remove all set keys as they throw an exception when used with withClaim
Expand Down
Loading
Loading