Skip to content
Merged
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: 2 additions & 0 deletions .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ on:
pull_request:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:

permissions:
contents: read
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## 2.5.0 - Unreleased

- Add durable, opaque, exact-once login intents for state, nonce, PKCE verifier,
allowlisted return paths, browser binding and correlation IDs.
- Require a shared atomic `LoginIntentStore` outside test and local-only
experiments; the in-memory store is explicitly unsuitable for production.
- Preserve the SDK 2.0 protocol surface while aligning the 2.5 Laravel guidance
with the published adapter's pending durable-intent upgrade.

## 2.0.0 - Unreleased

- Add typed PAR transactions and strict HTTPS transport.
Expand Down
20 changes: 17 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Official server-side relying-party SDK for Novvor Cloud Identity. It provides OI
discovery, Authorization Code plus PKCE, state and nonce validation, signed ID token
validation, JWKS rotation, assurance claims, and logout validation.

The upcoming 2.0 line also provides an opt-in high-assurance profile:
The 2.x line provides an opt-in high-assurance profile:
Authorization Code + PKCE S256 + PAR + JARM + RFC 9207 + DPoP +
`private_key_jwt`. It is designed for government and regulated enterprise
integrations without weakening the interoperable standard profile.
Expand All @@ -24,6 +24,16 @@ token validator. Never expose a client secret or token in browser code.

The administrative API is intentionally excluded from this package.

## Durable login intents (SDK 2.5)

`LoginIntentManager` preserves the internal application destination together
with `state`, `nonce`, and the PKCE verifier under an opaque, one-time handle.
Production applications must implement `LoginIntentStore` using a shared,
transactional database or cache. Cookies and callback query strings carry only
the opaque handle; they are never the authority for the return destination.

The included `InMemoryLoginIntentStore` exists only for tests and local
experiments. It is intentionally unsuitable for multiple processes or nodes.
## Security profiles

- `standard`: interoperable OIDC relying-party behavior. Existing 1.x
Expand All @@ -41,9 +51,13 @@ Never infer high-assurance support from a successful login. Discover metadata,
run `EnterpriseProfileValidator`, store the authorization transaction
server-side, and process callbacks through `AuthorizationResponseProcessor`.
See [the Laravel integration guide](docs/INTEGRATION_LARAVEL.md).
The cross-package adoption audit and release gates are in
The first-party `novvor/identity-laravel` adapter is published for SDK 2.0. Its
2.5 release must adopt durable login intents before consumers can claim the
2.5 integration contract. Until then Laravel applications must keep protocol
orchestration in one tested application adapter, never in controllers. The
cross-package adoption audit and 2.5 release gates are in
[`docs/SDK_2_ADOPTION_AUDIT.md`](docs/SDK_2_ADOPTION_AUDIT.md) and
[`docs/RELEASE_2_0_PLAN.md`](docs/RELEASE_2_0_PLAN.md).
[`docs/RELEASE_2_5_PLAN.md`](docs/RELEASE_2_5_PLAN.md).

## Workload authentication

Expand Down
16 changes: 14 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "novvor/identity-sdk-php",
"description": "Novvor Cloud Identity relying-party SDK for confidential server-side PHP applications.",
"description": "Novvor Cloud Identity SDK 2.5 for confidential server-side PHP applications.",
"type": "library",
"license": "Apache-2.0",
"require": {
Expand All @@ -18,10 +18,22 @@
"Novvor\\IdentitySdk\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-codex/identity-sdk-2": "2.5.x-dev"
}
},
"minimum-stability": "stable",
"prefer-stable": true,
"scripts": {
"analyse": "phpstan analyse --no-progress",
"test": "phpunit"
"test": "phpunit",
"verify": [
"@composer validate --strict",
"@test",
"@analyse",
"@composer audit",
"git diff --check"
]
}
}
34 changes: 22 additions & 12 deletions docs/INTEGRATION_LARAVEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,36 @@ Bind one `OidcClientConfiguration` from `config/identity.php`. Do not call
`env()` in controllers and do not provide `.test`, localhost or HTTP defaults
when `APP_ENV=production`.

Persist `state`, `nonce`, `code_verifier`, the intended destination and a
correlation ID in the encrypted server-side session. Do not serialize private
keys, tokens or authorization codes into the session.
Use `LoginIntentManager` with a shared, atomic `LoginIntentStore` to retain
`state`, `nonce`, `code_verifier`, the allowlisted intended destination and a
correlation ID. The browser-facing Laravel session may retain only the opaque
intent handle and a browser-session binding. Do not serialize protocol secrets,
private keys, tokens or authorization codes into a session or cookie.

## High-assurance sequence

```php
$transaction = $requests->transaction($configuration);
$intent = $loginIntents->begin(
transaction: $transaction,
returnPath: $allowlistedReturnPath,
browserBinding: session()->getId(),
correlationId: $correlationId,
);
$par = $pushed->push($configuration, $transaction, $discovery->pushedAuthorizationRequestEndpoint);

session()->put('identity.transaction', [
'state' => $transaction->state,
'nonce' => $transaction->nonce,
'code_verifier' => $transaction->codeVerifier,
]);
session()->put('identity.intent_handle', $intent->handle);

return redirect()->away($requests->pushedAuthorizationUrl($configuration, $par->requestUri));
```

The callback must use `AuthorizationResponseProcessor`, consume the transaction
once, exchange the code with `AuthorizationCodeClient`, validate the ID token
nonce, regenerate the Laravel session ID, bind the authenticated tenant and
only then restore the allowlisted destination.
The callback must obtain the intent handle from its server-side session,
atomically consume the intent before the code exchange, and use
`AuthorizationResponseProcessor`. It then exchanges the code with
`AuthorizationCodeClient`, validates the ID token nonce, regenerates the
Laravel session ID, binds the authenticated tenant and only then restores the
allowlisted destination. A consumed, expired or browser-binding-mismatched
intent must fail closed.

## Operational requirements

Expand All @@ -43,3 +50,6 @@ only then restore the allowlisted destination.
- Treat Identity unavailability as a bounded error surface, not a redirect
loop.
- Readiness must verify configuration and key presence without exposing values.
- Use `novvor/identity-laravel` v2.0.1 for SDK 2.0 integrations. Do not claim
its 2.5 contract until its durable-login-intent upgrade is published; never
duplicate protocol orchestration in controllers.
60 changes: 60 additions & 0 deletions docs/RELEASE_2_5_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Identity SDK 2.5 release plan

Date: 2026-08-01

## Release status

| Item | Status |
|---|---|
| `novvor/identity-contracts` v2.0.0 | Published baseline |
| `novvor/identity-sdk-php` v2.0.0 | Published baseline |
| SDK 2.5 core branch | Candidate; not tagged |
| First-party Laravel adapter | `v2.0.1` published; 2.5 durable-intent upgrade pending |
| Platform and FilaSign runtime upgrade | Not yet validated against 2.5 |
| Console v1-to-v2 migration | Not started |

The published adapter is an SDK 2.0 integration boundary. Its missing 2.5
durable-intent capability is a consumer rollout blocker, not a reason to weaken
the core release gate or duplicate protocol logic in controllers.

## Core release gate

Before tagging `v2.5.0`, the candidate must pass:

1. `composer validate --strict`.
2. A clean `composer install` using only published dependency tags.
3. `composer verify` (PHPUnit, PHPStan, Composer audit and `git diff --check`).
4. Positive and negative tests for login-intent exact-once consumption, browser
binding, expiry, state, nonce, PKCE, JARM, PAR, DPoP, RFC 9207 and token
validation.
5. A changelog that names any non-compatible behavior.
6. An immutable annotated Git tag and a clean installation test from that tag.

The `verify` script is the canonical local and CI-equivalent preflight. The
quality workflow also runs for immutable version tags so the release evidence
remains attached to the exact tagged source. A tag is not a deployment or
consumer-rollout approval.

## Consumer order

1. Publish the core SDK `v2.5.0` only after the core gate is green.
2. Publish a Laravel integration package that uses durable login intents and
makes the transaction lifecycle a single supported boundary.
3. Upgrade Enix Platform and FilaSign in independent branches; run their
browser and negative callback flows against the new package.
4. Migrate Enix Console from `^1.1` to `^2.5` in a separate review because it
is an authentication-boundary change, not a dependency bump.
5. Verify each deployment independently before the next consumer is changed.

## Rollback

Consumers retain their exact Composer lock until their independent runtime
validation passes. A failed consumer rollout is rolled back by restoring its
prior lockfile and deployment artifact; the immutable SDK tag is not rewritten.

## Truthful readiness

`identity-sdk-php` may become `PASS_PACKAGE_RELEASE` after its core gate. The
ecosystem becomes `PASS_RUNTIME` only after each consuming application proves
the configured OIDC flow, tenant binding, callbacks and negative cases at
runtime.
57 changes: 32 additions & 25 deletions docs/SDK_2_ADOPTION_AUDIT.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
# Identity SDK 2.0 adoption audit
# Identity SDK 2.5 adoption audit

Date: 2026-07-28

## Executive verdict

The PHP core now covers the relying-party protocol surface needed by Laravel
and framework-neutral backend consumers. Adoption was previously fragile
because consumers had to assemble transaction state and advanced protocol
features themselves. The official Laravel adapter now owns that orchestration.
The PHP core covers the relying-party protocol surface needed by Laravel and
framework-neutral backend consumers. It now includes durable, opaque login
intents so applications can keep a return destination, PKCE verifier, nonce and
state on the server under exact-once consumption semantics.

The first-party Laravel adapter exists and is released as `v2.0.1`. It uses an
encrypted Laravel session transaction and has not adopted the SDK 2.5 durable,
opaque login-intent contract. Therefore a consumer-wide Laravel upgrade to 2.5
is not releasable until the adapter is updated, tested and released.

This is `PASS_LOCAL_INTEGRATION`, not OpenID certification or production proof.

Expand All @@ -17,7 +22,7 @@ This is `PASS_LOCAL_INTEGRATION`, not OpenID certification or production proof.
|---|---|---|
| `identity-contracts` | claim names and security profiles | transport, secrets |
| `identity-sdk-php` | framework-neutral OAuth/OIDC protocol | Laravel session, admin APIs |
| `identity-laravel` | Laravel config, DI and transaction lifecycle | tenant authorization policy |
| `identity-laravel` | Laravel config, DI and transaction lifecycle; published `v2.0.1`, 2.5 durable-intent upgrade pending | tenant authorization policy |
| `identity-admin-sdk-php` | privileged control-plane transport | user login/session logic |
| `identity-sdk-testing` | truthful fakes and negative fixtures | real keys, tokens, customer data |

Expand All @@ -37,21 +42,23 @@ This is `PASS_LOCAL_INTEGRATION`, not OpenID certification or production proof.
11. Bind UserInfo `sub` to the ID Token `sub`.
12. Map tenant and permissions in the application, then regenerate its session.

Laravel consumers should use `novvor/identity-laravel` rather than reproduce
these steps in controllers.
Laravel consumers must use the published adapter for SDK 2.0 or one tested
application integration service. The adapter requires a 2.5 update and release
against `LoginIntentManager` before it can be treated as the consumer-wide 2.5
contract; controllers must not reconstruct this flow.

## Capability truth

| Capability | Core | Laravel adapter | Server evidence required |
| Capability | Core | Laravel integration boundary | Server evidence required |
|---|---:|---:|---|
| Authorization Code | yes | orchestrated | response type |
| PKCE S256 | yes | orchestrated | S256 metadata |
| state / nonce | yes | encrypted session | runtime callback |
| RFC 9207 | yes | enforced | issuer parameter |
| PAR | yes | orchestrated | PAR endpoint |
| JARM | yes | orchestrated | query.jwt + RS256 |
| DPoP | yes | per-session ES256 key | ES256 + DPoP token |
| private_key_jwt | yes | configured | registered public key |
| Authorization Code | yes | shared app service required | response type |
| PKCE S256 | yes | shared app service required | S256 metadata |
| state / nonce | yes | durable login intent required | runtime callback |
| RFC 9207 | yes | shared app service required | issuer parameter |
| PAR | yes | shared app service required | PAR endpoint |
| JARM | yes | shared app service required | query.jwt + RS256 |
| DPoP | yes | per-session ES256 key required | ES256 + DPoP token |
| private_key_jwt | yes | configured by app | registered public key |
| refresh rotation | yes | bound client | replacement refresh token |
| UserInfo | yes | subject-bound | endpoint |
| introspection/revocation | yes | bound clients | endpoints |
Expand All @@ -61,13 +68,13 @@ these steps in controllers.

## Remaining release blockers

1. Tag `identity-contracts` 2.0.
2. Tag `identity-sdk-php` 2.0.
3. Replace the Laravel adapter's temporary Draft-branch constraint with `^2.0`.
4. Tag `identity-sdk-testing` and `identity-admin-sdk-php` 2.0.
5. Run a clean Composer install using tags only.
6. Validate one Laravel reference consumer end to end.
7. Run negative issuer, callback replay, tenant mismatch and key-rotation tests.
8. Validate staging runtime and an external OpenID conformance profile.
1. Complete the 2.5 core release gate and publish an immutable `v2.5.0` tag.
2. Update, release and test the first-party Laravel adapter against durable
`LoginIntentManager` storage; retain its current 2.0 contract until then.
3. Run a clean Composer install using tags only.
4. Validate Platform and FilaSign as reference consumers end to end.
5. Migrate Console from the v1 line as a separate, explicitly reviewed change.
6. Run negative issuer, callback replay, tenant mismatch and key-rotation tests.
7. Validate staging runtime and an external OpenID conformance profile.

No package should claim `PASS_RUNTIME` until those external gates have evidence.
37 changes: 37 additions & 0 deletions src/Oidc/InMemoryLoginIntentStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace Novvor\IdentitySdk\Oidc;

/**
* Testing/local adapter only. Production consumers must provide a durable,
* shared implementation backed by their database or transactional cache.
*/
final class InMemoryLoginIntentStore implements LoginIntentStore
{
/** @var array<string, LoginIntent> */
private array $intents = [];

public function put(LoginIntent $intent): void
{
if (isset($this->intents[$intent->handle])) {
throw new OidcException('Login intent handle already exists.');
}

$this->intents[$intent->handle] = $intent;
}

public function get(string $handle): ?LoginIntent
{
return $this->intents[$handle] ?? null;
}

public function consume(string $handle): ?LoginIntent
{
$intent = $this->intents[$handle] ?? null;
unset($this->intents[$handle]);

return $intent;
}
}
41 changes: 41 additions & 0 deletions src/Oidc/LoginIntent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Novvor\IdentitySdk\Oidc;

final readonly class LoginIntent
{
public function __construct(
public string $handle,
public string $state,
public string $nonce,
public string $codeVerifier,
public string $returnPath,
public string $browserBinding,
public string $correlationId,
public int $createdAt,
public int $expiresAt,
) {
if (! preg_match('/^[A-Za-z0-9_-]{43,128}$/', $handle)) {
throw new OidcException('Login intent handle is invalid.');
}
if ($state === '' || $nonce === '' || $codeVerifier === '') {
throw new OidcException('Login intent protocol values are required.');
}
if ($returnPath === '' || $returnPath[0] !== '/' || str_starts_with($returnPath, '//')) {
throw new OidcException('Login intent return path must be application-relative.');
}
if ($browserBinding === '' || $correlationId === '') {
throw new OidcException('Login intent binding and correlation are required.');
}
if ($expiresAt <= $createdAt) {
throw new OidcException('Login intent lifetime is invalid.');
}
}

public function isExpired(int $now): bool
{
return $now >= $this->expiresAt;
}
}
Loading
Loading