Releases: fleetbase/solid
Release list
v0.0.8
v0.0.8 ~ "Solid-OIDC brought in-house, and a verified authorization callback"
Highlights
Solid can be installed alongside current Core API again. jumbojett/openid-connect-php pinned phpseclib/phpseclib ^3.0.7, and Core API's laravel/socialite ^5.31 requires ^4.0, so the two could not coexist — Composer refused to resolve the application at all. Upstream Jumbojett has not migrated to phpseclib 4, and phpseclib 4 renamed its root namespace, so widening the constraint would resolve and then fail at runtime.
The dependency turned out to be carrying that pin for code Solid never ran. Jumbojett touches phpseclib in one method, RSA signature verification, which this extension already overrode — and the authorization callback never reached Jumbojett's verification at all. Roughly 150 lines of a 2,100-line library were in use, with transport, token requests, client registration, session storage and discovery all replaced locally.
So the library is gone and the Solid-OIDC client lives in the extension, in server/src/Client/OpenIDConnectClient.php and server/src/Auth/. Solid-OIDC needs three things no general-purpose OAuth client provides — RFC 7591 dynamic client registration, RFC 9449 DPoP-bound tokens, and an issuer discovered per tenant at runtime — and all three were already written here. ID token verification follows the conventions of Core API's Fleetbase\Auth\OAuth\IdTokenVerifier.
Closing that out fixed a set of real gaps in the handshake, described below. No user has to sign in again: existing client registrations are reused and existing DPoP keys are migrated rather than regenerated.
Security
The authorization callback now verifies what it receives. Previously it exchanged whatever code it was handed, and the WebID that drives every pod read and write was taken from an unverified JWT.
- ID token verification. Signature checked against the provider's published JWKS, plus
iss(exact),aud,azpwhenaudis multi-valued, expiry with a bounded 60-second leeway,nonce, and the presence ofsub. None of these were checked before. statevalidation. Server-side, single use, consumed before comparison so a rejected callback cannot be retried, and a mismatch now voids the whole pending request.statewas previously ignored entirely.- DPoP private keys are no longer web-reachable. They were written with a bare
Storage::put(), and the application's default disk ispublic— rooted atstorage/app/public, symlinked topublic/storageand served over HTTP — or an S3/GCS bucket. Keys now go to an explicitly named private disk (solid.oidc.key_disk, defaultlocal), and a key found in the old location is moved there and the exposed copy deleted. - Algorithm confusion. A key without an
algis dropped rather than assumed to be RS256, and a token whose header algorithm differs from its key's is rejected, soalg: noneand HS256-signed-with-the-RSA-public-key both fail. - PKCE is mandatory and fails loudly if the provider advertises methods that exclude S256, instead of silently dropping the challenge.
- TLS verification is one explicit flag,
solid.oidc.verify_tls, rather than inferred fromAPP_ENV— which also disabled it in any environment an operator happened to namelocalordevelopment. Local development against a self-signed certificate is unchanged by default; setSOLID_OIDC_VERIFY_TLS=falseto say so explicitly. - Redirects are no longer followed on OIDC requests. The old transport set
CURLOPT_FOLLOWLOCATION, which could replay anAuthorization: Basicclient secret against whatever host aLocationheader named. - Tokens are out of the logs. The Solid client was logging the decoded access-token payload and then the whole header set, including
Authorization: DPoP <token>and the DPoP proof. state,nonceand the PKCE verifier now expire. They were written to Redis with no TTL, so an abandoned sign-in left them readable indefinitely.- The access token's
cnf.jktis checked against the DPoP key held for the identity, so a token bound to a different key fails at sign-in rather than on every subsequent request.
Fixes
- DPoP
htustrips the query and fragment, as RFC 9449 §4.2 requires. A proof built for a URL with parameters was unusable against a server that compareshtustrictly. - RFC 9449 §8
use_dpop_nonceis honoured: the token request is retried once with the nonce the provider asks for. - Provider discovery is cached, per process and in the application cache. Every Solid request built a fresh client and refetched
.well-known/openid-configuration, so a single controller action touching four resources made four extra round trips. - The authorization callback reports a provider
errorinstead of turning it into "missing authorization code". authenticate()returns a redirect response rather than callingheader()andexit, so the redirect goes through the framework.- Dynamic client registration declares
grant_typesincludingrefresh_token. Without it a provider defaults toauthorization_codealone, which made the requestedoffline_accessscope unusable. Existing registrations are untouched; this applies to new ones. - Requested scopes are deduplicated — the authorization request was sending
openid webid offline_access openid. firebase/php-jwtis declared. It was already in use but relied on arriving transitively.
Dependencies
- Removed:
jumbojett/openid-connect-php, all sevenweb-token/jwt-*packages,php-http/guzzle7-adapter,psr/http-factory-implementation. Theweb-tokenstack and the PSR adapters it needed had no references anywhere in the extension. - Added:
firebase/php-jwt ^6.10|^7.0(shared with Socialite rather than duplicating a JWT stack),ext-json,ext-openssl. phpraised from^8.0to^8.1, matching Core API and phpseclib 4.- Engine dependencies moved to their latest releases:
@fleetbase/ember-core^0.3.24,@fleetbase/ember-ui^0.4.3,@fleetbase/fleetops-data^0.2.2. CI builds on Node 22.
Also fixed
Four defects that predate this release, found while auditing the OIDC code:
PodServiceresolved a class that does not exist. The commit that removed the CSS-credential approach in favour of OIDC tokens deletedCssAccountServicebut left twoapp(CssAccountService::class)call sites behind, so pod creation and the pod listing threw on entry. Those branches are removed, finishing that change.getAccountIndex()ended indd()on the liveGET solid/int/v1/accountroute, halting the request and dumping the raw pod response. It returns JSON now. Seven more routes pointed at controller methods that do not exist (play,getProfileDataand the fivesync-*actions) and would 500 on hit; nothing calls them, so they are removed. Every routed method now resolves.- The admin server-configuration UI had no effect.
saveServerConfig()wrotesystem.solid.serverandSolidClientnever read it, so changing the Solid host or port in the console changed nothing. Resolution is now explicit option → saved setting → config default. Two bugs in the same lines went with it: a caller-supplied host was silently discarded, andnew SolidClient([])raised aTypeError. composer testfailed in all three stages. It passes now:test:unitis back on Pest through a runner that works around the package's customvendor-dir,test:typesis clean at phpstanlevel: maxagainst a committed baseline, and the six long-unformatted files are formatted. CI enforces all three, andRun Lintis now the dry-run form — it previously ran php-cs-fixer in fix mode, so it could never fail.
Coverage is now generated, uploaded to Codecov and shown as a README badge: 25.45% overall, with the code added here at 81–100%.
Tests
The suite went from one placeholder test to 108 tests / 208 assertions, covering ID token verification and its failure modes, the full authorization and callback flow, state and nonce handling, DPoP proof structure and key storage, JWKS caching and rotation, and dynamic client registration. Verified on PHP 8.2 and 8.4.
The suite had been disabled in CI because Pest's binary resolves its autoloader from a hardcoded vendor/, which this package does not have — it sets vendor-dir to server_vendor — so pest could not start here at all. composer test:unit now goes through scripts/pest-runner.php, adopted from storefront, which bridges that for the duration of the run. CI runs the tests again, plus static analysis, lint and coverage.
Upgrading
- Root
composer.json:"fleetbase/solid-api": "^0.0.8". A^0.0.7constraint will not pick this up. exchangeCodeForTokens($code, $state)now requires thestatefrom the callback. The signature is unchanged, but a missing one throws — without it there is no CSRF control on the callback.Jumbojett\OpenIDConnectClientExceptionis replaced byFleetbase\Solid\Exceptions\OpenIDConnectClientException, same name and same\Exceptionparent.- If
FILESYSTEM_DRIVERpoints at S3 or GCS, check that bucket for existingsolid/dpop_keys_*.jsonobjects and delete them. The automatic migration only covers the disk the application is currently configured with.
What's Changed
- Fix critical and high-priority bugs by @roncodes in #8
- Fix the phpseclib conflict: own the Solid-OIDC client, verify the callback, and adopt release/v* tagging by @roncodes in #9
Full Changelog: v0.0.7...v0.0.8
v0.0.7
What's Changed
Full Changelog: v0.0.6...v0.0.7
v0.0.6
What's Changed
Full Changelog: v0.0.5...v0.0.6
v0.0.5
v0.0.4
v0.0.3
- Created UI for pods management and browsing and identity management
- UI for create new pod
- UI for pod naming
- UI for viewing files and folders within pod
- UI for deleting pods
- UI for syncing pod data
- UI for backing up pod
- UI to view data within pods
What's Changed
Full Changelog: v0.0.2...v0.0.3
v0.0.2
- Added UI in admin for managing Solid server configuration.
- Began development of UI for managing Solid pods.
- Refactored Solid Client for Fleetbase
What's Changed
New Contributors
Full Changelog: https://github.com/fleetbase/solid/commits/v0.0.2