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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 2.0.0 - Unreleased

- Add typed PAR transactions and strict HTTPS transport.
- Add JARM validation pinned to RS256, exact issuer and client audience.
- Require RFC 9207 `iss` and exact `state` on authorization responses.
- Add DPoP proof creation with `htu`, `htm`, `jti`, `iat`, `ath` and nonce.
- Retry one valid RFC 9449 `use_dpop_nonce` challenge at token, refresh and
UserInfo endpoints; repeated or malformed challenges fail closed.
- Add interactive `private_key_jwt` client authentication.
- Add typed refresh rotation, UserInfo, token introspection and revocation clients.
- Bind refresh and UserInfo requests to DPoP keys and enforce UserInfo subject matching.
- Surface OAuth errors with redacted correlation and DPoP nonce metadata.
- Add an opt-in Novvor high-assurance profile with downgrade prevention.
- Preserve 1.x secret/no-secret behavior through the `auto` compatibility mode.
- Expand Discovery parsing without advertising capabilities on behalf of the
authorization server.

## 1.2.0 - 2026-07-27

- Add confidential workload client-credentials issuance.
Expand Down
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ 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:
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.

It is not an Identity server or an administrative API client.

## Installation
Expand All @@ -19,6 +24,27 @@ token validator. Never expose a client secret or token in browser code.

The administrative API is intentionally excluded from this package.

## Security profiles

- `standard`: interoperable OIDC relying-party behavior. Existing 1.x
configurations remain compatible; `auto` uses `client_secret_post` only when
a secret was supplied.
- `novvor-high-assurance-v1`: fail-closed profile requiring PAR, JARM,
`private_key_jwt`, RFC 9207 issuer binding and a DPoP-bound access token.

Token, refresh and UserInfo clients automatically answer one valid RFC 9449
`use_dpop_nonce` challenge. Repeated, unchanged, malformed or oversized nonce
challenges fail closed, so a broken server cannot create an unbounded retry
loop.

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
[`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).

## Workload authentication

`ClientCredentialsClient` issues server-to-server access tokens using the
Expand All @@ -45,3 +71,5 @@ Only explicit HTTPS endpoints are accepted. JWT signing is pinned to RS256, keys
are resolved by `kid`, and issuer, audience, authorized party, nonce, time, and
back-channel logout claims are validated. Consumers must prevent replay of logout
`jti` values in their shared cache. See [`SECURITY.md`](SECURITY.md).
The capability truth table and downgrade boundaries are documented in
[`docs/SECURITY_PROFILE.md`](docs/SECURITY_PROFILE.md).
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"php": "^8.2",
"firebase/php-jwt": "^7.0",
"guzzlehttp/guzzle": "^7.0",
"novvor/identity-contracts": "^1.0"
"novvor/identity-contracts": "^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
Expand Down
24 changes: 24 additions & 0 deletions docs/CAPABILITY_MATRIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Capability matrix

| Capability | SDK 2.0 | Required server evidence |
| --- | --- | --- |
| Authorization Code | Implemented and tested | `response_types_supported` |
| PKCE S256 | Implemented and tested | `code_challenge_methods_supported` |
| RFC 9207 | Implemented and tested | issuer parameter support |
| PAR | Implemented and tested | PAR endpoint |
| JARM query.jwt | Implemented and tested | response mode and RS256 |
| private_key_jwt | Implemented and tested | auth method plus registered key |
| DPoP | Implemented and tested | signing algorithms and DPoP token type |
| ID token validation | Implemented and tested | issuer, JWKS and RS256 |
| Refresh token rotation | Implemented and tested | `refresh_token` grant and replacement token |
| UserInfo | Implemented and tested | UserInfo endpoint and subject binding |
| Token introspection | Implemented and tested | introspection endpoint and client authentication |
| Token revocation | Implemented and tested | revocation endpoint and client authentication |
| DPoP nonce challenge | Implemented and tested | RFC 9449 `use_dpop_nonce` plus `DPoP-Nonce` |
| Back-channel logout | Implemented and tested | server metadata and replay store |
| Front-channel logout | Not implemented | must not be inferred |
| Dynamic client registration | Not implemented | use governed admin plane |

“Implemented” describes SDK capability only. Consumers must not advertise or
enable a feature until the target Identity environment proves matching runtime
support.
45 changes: 45 additions & 0 deletions docs/INTEGRATION_LARAVEL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Laravel integration

## Ownership

The SDK owns protocol construction and validation. Laravel owns configuration,
session storage, cache, redirects, logging, dependency injection and secret
retrieval.

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.

## High-assurance sequence

```php
$transaction = $requests->transaction($configuration);
$par = $pushed->push($configuration, $transaction, $discovery->pushedAuthorizationRequestEndpoint);

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

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.

## Operational requirements

- HTTPS and certificate verification are mandatory.
- Use shared cache for one-time transaction consumption and replay detection.
- Redact tokens, assertions, codes and private keys from logs and telemetry.
- Propagate a validated correlation ID.
- Treat Identity unavailability as a bounded error surface, not a redirect
loop.
- Readiness must verify configuration and key presence without exposing values.
27 changes: 27 additions & 0 deletions docs/MIGRATION_V1_TO_V2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Migration from 1.x to 2.0

## Compatibility

Existing constructor calls remain valid. The default client authentication mode
is `auto`: a configured secret retains the historical
`client_secret_post` behavior; a client without a secret remains public.

No existing application is automatically promoted to the high-assurance
profile. Promotion must be coordinated with the Identity client registration.

## Governed migration

1. Upgrade the package while retaining `standard`.
2. Replace direct callback parsing with `AuthorizationResponseProcessor`.
3. Require RFC 9207 `iss` in callbacks.
4. Provision a dedicated asymmetric client key and register its public key.
5. Confirm Discovery advertises PAR, `query.jwt`, `private_key_jwt`, DPoP and
authorization-response issuer support.
6. Enable `novvor-high-assurance-v1`.
7. Push the request with `PushedAuthorizationClient`.
8. Redirect with only `client_id` and `request_uri`.
9. Validate JARM and then exchange the code with a DPoP key.
10. Verify negative tests before production promotion.

Roll back by reverting the client to `standard`; do not retain a partly enabled
profile or selectively disable one of its controls.
43 changes: 43 additions & 0 deletions docs/RELEASE_2_0_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Identity SDK 2.0 release plan

## Version and publication order

1. `novvor/identity-contracts` 2.0.0
2. `novvor/identity-sdk-php` 2.0.0
3. `novvor/identity-sdk-testing` 2.0.0
4. `novvor/identity-admin-sdk-php` 2.0.0
5. `novvor/identity-laravel` 2.0.0

Every tag must be immutable, signed when repository policy permits, and created
from a reviewed commit with Composer validation, audit, static analysis and
tests passing. Consumers must depend on semantic tags, never branch aliases.

## Compatibility policy

- 2.x may add optional endpoints and typed methods without breaking callers.
- Protocol downgrade, weaker validation or expanded trust is never a compatible
change.
- Claim removal, constructor changes and authentication-method changes require
a major release.
- Security fixes receive a patch release and a coordinated advisory when
disclosure is warranted.

## Staging gate

- exact issuer and callbacks;
- TLS verification;
- standard and high-assurance positive flow;
- callback replay rejection;
- wrong issuer/audience/nonce rejection;
- DPoP key mismatch rejection;
- refresh reuse family revocation;
- JWKS rollover;
- tenant mismatch rejection;
- sanitized logs and correlation IDs;
- clean install using only release tags.

## Rollback

Keep the previous minor tag installable and retain the previous signing public
key for the governed overlap window. Roll back consumers before retiring a key
or contract version. Never re-tag an existing version.
73 changes: 73 additions & 0 deletions docs/SDK_2_ADOPTION_AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Identity SDK 2.0 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.

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

## Package boundary

| Package | Public contract | Must not contain |
|---|---|---|
| `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-admin-sdk-php` | privileged control-plane transport | user login/session logic |
| `identity-sdk-testing` | truthful fakes and negative fixtures | real keys, tokens, customer data |

## Required consumer flow

1. Resolve an explicit canonical issuer and endpoints.
2. Discover metadata and compare the issuer exactly.
3. Validate the selected profile before browser redirect.
4. Generate state, nonce and PKCE S256.
5. For high assurance, create a per-session DPoP key and push the request with
PAR authenticated by `private_key_jwt`.
6. Redirect using only `client_id` and the PAR `request_uri`.
7. Validate JARM RS256, RFC 9207 issuer and state.
8. Consume the one-time transaction before exchanging the code.
9. Exchange with the exact redirect URI, PKCE verifier and DPoP proof.
10. Validate ID Token issuer, audience, `azp`, signature, time and nonce.
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.

## Capability truth

| Capability | Core | Laravel adapter | 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 |
| refresh rotation | yes | bound client | replacement refresh token |
| UserInfo | yes | subject-bound | endpoint |
| introspection/revocation | yes | bound clients | endpoints |
| DPoP nonce challenge | yes | delegated to core | `use_dpop_nonce` + `DPoP-Nonce` |
| front-channel logout | no | no | do not advertise |
| dynamic registration | no | no | use admin control plane |

## 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.

No package should claim `PASS_RUNTIME` until those external gates have evidence.
42 changes: 42 additions & 0 deletions docs/SECURITY_PROFILE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Novvor Identity SDK 2.0 security profile

## Profile contract

The `novvor-high-assurance-v1` profile is an explicit, fail-closed contract:

1. Authorization Code Flow only.
2. PKCE with S256.
3. Pushed Authorization Requests (PAR).
4. JWT Secured Authorization Response Mode (JARM, `query.jwt`).
5. RFC 9207 authorization-response issuer binding.
6. `private_key_jwt` client authentication.
7. DPoP proof at the token endpoint and a `DPoP` token type.
8. Exact redirect URI, state, nonce, issuer, audience, expiry and signing-key
validation.

The SDK does not silently downgrade to an ordinary query response, bearer
token, shared secret, missing issuer, unknown algorithm or unknown signing key.

## Threat boundaries

| Threat | Control |
| --- | --- |
| Authorization request tampering | PAR and server-side transaction state |
| Callback mix-up | RFC 9207 exact issuer plus state |
| Response tampering | JARM RS256 signature and claims |
| Code interception | PKCE S256 |
| Client impersonation | short-lived `private_key_jwt` with unique `jti` |
| Access-token replay | DPoP proof bound to method, URI and token hash |
| Key confusion | RS256 allowlist and exact `kid` |
| Redirect abuse | exact redirect URI registered at Identity |

DPoP is defense in depth, not a replacement for TLS, strict redirect
registration, short token lifetimes or server-side replay controls.

## Key custody

The SDK accepts private key material from the host application but never
persists it. Production applications should obtain keys from their secret
manager or KMS integration, keep distinct keys per client and environment, and
rotate using overlapping public keys. No private key belongs in Git, logs,
exceptions, cache payloads or browser code.
Loading
Loading