Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -86,7 +86,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 @@ -153,12 +154,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
2 changes: 1 addition & 1 deletion src/RemoteCall/DeleteInstanceTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ protected function exitRemoteCall(): void
exit;
}

private function deleteInstance(string $instanceId, RemoteCallInterface $remoteCallHandler): void
private function deleteInstance(string $instanceId, RemoteCallInterface $remoteCallHandler): never
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Changing deleteInstance() to return never can cause a runtime fatal error if a consumer overrides exitRemoteCall() (it’s protected) with an implementation that does not terminate execution. In that case, deleteInstance() would reach the end and violate the never contract. To avoid breaking extensibility/tests, consider keeping the return type void and using a PHPDoc @return never for static analysis, or alternatively also declare exitRemoteCall(): never to enforce the contract consistently.

Suggested change
private function deleteInstance(string $instanceId, RemoteCallInterface $remoteCallHandler): never
/**
* 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

Copilot uses AI. Check for mistakes.
{
if ($remoteCallHandler instanceof DeleteInstanceCallHandlerInterface) {
$result = $remoteCallHandler->deleteInstance($instanceId);
Expand Down
6 changes: 4 additions & 2 deletions src/SSOData/ClaimAccessTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,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 @@ -32,7 +32,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 @@ -42,7 +43,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 @@ -52,7 +54,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 @@ -64,7 +67,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 @@ -74,7 +78,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 @@ -84,7 +89,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 @@ -97,7 +103,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 @@ -107,7 +114,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 @@ -117,7 +125,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 @@ -127,7 +136,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 @@ -137,7 +147,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 @@ -147,20 +158,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 @@ -172,7 +185,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 @@ -184,7 +198,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 @@ -194,7 +209,8 @@ public function getThemeBackgroundColor(): ?string
*/
public function getLocale(): string
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LOCALE);
$val = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LOCALE);
return is_string($val) ? $val : '';
Comment on lines +212 to +213
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$val = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LOCALE);
return is_string($val) ? $val : '';
$value = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_LOCALE);
return is_string($value) ? $value : '';

}

/**
Expand All @@ -204,6 +220,7 @@ public function getLocale(): string
*/
public function getTags(): ?array
{
return $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_TAGS);
$val = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_TAGS);
return is_array($val) ? $val : null;
Comment on lines +223 to +224
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$val = $this->getClaimSafe(SSODataClaimsInterface::CLAIM_USER_TAGS);
return is_array($val) ? $val : null;
$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 @@ -59,7 +59,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 @@ -69,7 +70,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 @@ -79,7 +81,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 @@ -89,7 +92,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 @@ -99,7 +103,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 @@ -109,21 +114,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
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 a "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 @@ -64,22 +64,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');
}
Comment on lines +67 to +71
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

aud is validated as a non-empty string, but aud is commonly represented as an array (and SharedDataTrait::getAudience() explicitly supports array audiences). This makes token generation stricter/inconsistent with token parsing. Consider accepting string|array<string> here (e.g. if array, call permittedFor() for each value or pick the first) instead of throwing.

Copilot uses AI. Check for mistakes.

$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