Conversation
This PR migrates the authentication system from the custom OpenID Connect implementation to the well-maintained oidc library, addressing several security and maintainability concerns. Key changes: - Replace custom OpenID client with oidc library for better security - Add reactive authentication state management with ValueNotifier - Implement proper DPoP token handling through oidc hooks - Refactor example app to demonstrate new authentication patterns - Add client-profile.jsonld for Solid OIDC compliance - Update dependencies and remove unnused packages Breaking changes: - New SolidAuth class replaces previous authenticate() function - Authentication state is now reactive via isAuthenticatedNotifier - DPoP token generation integrated into authentication flow This migration improves security, reduces maintenance burden, and provides a more modern Flutter-friendly API while maintaining full Solid OIDC compatibility. The changes have been tested with the example application. Resolves authentication reliability issues and provides a foundation for future enhancements to the library.
… offline_access Previously, Solid OIDC authentication failed to obtain refresh tokens because the required 'consent' prompt wasn't sent to identity providers. This change automatically adds the consent prompt when offline_access is in the requested scopes, enabling proper token refresh and persistent authentication. Includes configurable prompt calculation and comprehensive test coverage.
- Change default value of strictJwtVerification from false to true - Update documentation to reflect security-first approach - Add security warnings for disabling strict verification - Remove redundant examples showing explicit true value
During refresh, apparently we do not have headers on the request, so we need to set it to an empty map
Added required network permissions documentation to README: - Android: INTERNET permission in AndroidManifest.xml - macOS: com.apple.security.network.client entitlement These permissions are essential for Solid authentication to work properly when connecting to identity providers and pod servers. Co-Authored-By: Claude <noreply@anthropic.com>
Add DpopCredentials class for safe intra-process transfer of cryptographic material to Dart isolates and web workers. This enables offloading DPoP token generation to worker threads for improved performance in high- throughput scenarios. - DpopCredentials: Immutable, serializable class with RSA keys and access token - exportDpopCredentials(): Export credentials from SolidAuth/SolidOidcUserManager - generateDpopToken(): Instance method on DpopCredentials for token generation - Comprehensive security documentation explaining intra-process trust model - Complete isolate example (example/dpop_worker_example.dart) - Test coverage (7 tests for DpopCredentials) The security model is based on standard practice in multi-threaded crypto libraries: credentials stay within the OS-protected process boundary.
PROBLEM: DPoP credentials needed to be transferred to web workers and isolates for performance-optimized token generation. However, using fast_rsa's KeyPair class in our serializable DpopCredentials prevented this because fast_rsa types depend on platform-specific code (platform channels on native, WebAssembly on web) that is unavailable in worker contexts. The root cause was subtle but simple: we never actually needed to *call* fast_rsa methods in workers - we only needed to *use* the KeyPair type to store PEM strings. The dependency on the fast_rsa.KeyPair type itself was the blocker. SOLUTION: Introduce a clean abstraction layer that isolates fast_rsa to key generation: 1. Created platform-agnostic types in rsa_api.dart: - KeyPair: Simple PEM string container (no platform dependencies) - GeneratedRsaKeyPair: Complete generation result with JWK - RsaCrypto: Abstract interface for key generation 2. Implemented fast_rsa adapter in rsa_fast.dart: - RsaCryptoImpl wraps fast_rsa for all platforms - Converts fast_rsa.KeyPair to our platform-agnostic KeyPair - Maintains excellent performance (native code + WebAssembly) 3. Created singleton in rsa_impl.dart: - Provides single point of access via 'rsa' constant - Ready for platform-specific implementations if needed 4. Updated all consumers to use new types: - DpopCredentials now uses KeyPair instead of fast_rsa.KeyPair - SolidOidcUserManager adapted to new API - Removed genRsaKeyPair() helper (now rsa.generate()) BENEFITS: - DPoP credentials can now be safely serialized and transferred to workers - Clear separation between interface and implementation - Preserves fast_rsa performance on all platforms - No behavioral changes to existing functionality - Simpler than initially anticipated - no need for web-specific implementation KEY INSIGHT: The abstraction isn't about replacing fast_rsa - it's about making the *results* of fast_rsa operations portable across execution contexts by using simple, serializable types (strings) instead of platform-specific classes.
Architectural Improvements: ========================== Split the library into two entry points to enable DPoP token generation in Dart isolates and web workers without Flutter dependencies: 1. package:solid_auth/solid_auth.dart (Main entry point) - Full Flutter-enabled authentication library - SolidAuth class with UI integration - OIDC flow management with browser redirects - Session persistence using platform storage - Exports: SolidAuth, UserAndWebId, DpopCredentials, DPoP 2. package:solid_auth/worker.dart (Worker entry point - NEW) - Pure Dart, zero Flutter dependencies - Minimal API for DPoP token generation - Safe for use in isolates and web workers - Exports: DpopCredentials, DPoP, KeyPair File Structure Changes: ====================== Moved/Created: - lib/worker.dart (NEW) - Flutter-free public API - lib/src/gen_dpop_token.dart (MOVED from solid_auth_client.dart) - lib/src/oidc/dpop_credentials.dart (MOVED from solid_oidc_user_manager.dart) - example/lib/dpop_worker_example.dart (MOVED from example/) Deleted: - lib/src/solid_auth_client.dart (split into gen_dpop_token.dart + imports) - example/dpop_worker_example.dart (moved to example/lib/) Modified: - lib/src/oidc/solid_oidc_user_manager.dart - Removed DpopCredentials and DPoP classes (moved to dpop_credentials.dart) - Updated imports to use gen_dpop_token.dart - lib/src/solid_auth.dart - Updated exports to reflect new file structure - test/dpop_credentials_test.dart - Updated import to use new dpop_credentials.dart location Benefits: ======== Performance: - Offload cryptographic operations from main UI thread - Generate multiple DPoP tokens in parallel - Better responsiveness for high-throughput scenarios Architecture: - Clear separation of concerns (Flutter vs. pure Dart) - Enables testing without Flutter test harness - Potential reuse in non-Flutter Dart projects Developer Experience: - Explicit import paths prevent accidental Flutter deps in workers - Comprehensive documentation in worker.dart - Complete working examples Documentation: ============= Added comprehensive documentation covering: - worker.dart: 90+ lines explaining architecture and usage - gen_dpop_token.dart: RFC references and internal API docs - README.md: Complete worker thread section with examples - example/lib/dpop_worker_example.dart: Full working patterns Security Model: ============== Maintains existing security posture: - Credentials safe for intra-process transfer only - No persistent storage of serialized credentials - Fresh token generation per request - Private keys never leave credential objects Testing: ======= All existing tests pass: - test/dpop_credentials_test.dart: 7/7 tests passing - No behavioral changes to public API - Example code analyzes without issues
There was a problem hiding this comment.
Pull request overview
This PR introduces a worker/isolate-friendly API for DPoP token generation by exporting serializable credentials (DpopCredentials) and providing a Flutter-free entrypoint (worker.dart), alongside the broader migration away from the embedded OpenID implementation to the oidc package.
Changes:
- Add
DpopCredentials+worker.dartentrypoint to generate DPoP tokens off the main thread (isolates / web workers). - Introduce platform-agnostic RSA key types and extract DPoP generation into a Flutter-free module.
- Update docs, tests, CI workflow, and the example app to reflect the new OIDC-based architecture.
Reviewed changes
Copilot reviewed 47 out of 48 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| pubspec.yaml | Bumps version and updates dependencies to oidc + adjusts crypto deps. |
| lib/worker.dart | New Flutter-free public entrypoint for isolate/worker DPoP generation. |
| lib/src/solid_auth.dart | Adds exportDpopCredentials() and wires SolidAuth to SolidOidcUserManager. |
| lib/src/solid_auth_issuer.dart | Converts issuer discovery helper into a standalone module (no longer a part). |
| lib/src/oidc/dpop_credentials.dart | Adds serializable credential container + thread-safe DPoP generation API. |
| lib/src/gen_dpop_token.dart | Extracts core DPoP JWT creation/signing into a Flutter-free internal module. |
| lib/src/rsa/rsa_api.dart | Adds platform-agnostic KeyPair + generated-key result types. |
| lib/src/rsa/rsa_fast.dart | Adds fast_rsa-backed RSA generation implementation returning platform-agnostic types. |
| lib/src/rsa/rsa_impl.dart | Exposes a singleton RSA implementation used by the library. |
| lib/solid_auth.dart | Replaces old part-based layout with a single export of src/solid_auth.dart. |
| lib/solid_auth_client.dart | Removes legacy Solid auth implementation (dynamic reg + embedded flow). |
| lib/src/openid/openid_client.dart | Removes embedded openid_client entrypoint. |
| lib/src/openid/openid_client_browser.dart | Removes embedded browser authenticator implementation. |
| lib/src/openid/openid_client_io.dart | Removes embedded IO authenticator implementation. |
| lib/src/openid/src/openid.dart | Removes embedded OpenID provider/client/flow implementation. |
| lib/src/openid/src/http_util.dart | Removes embedded HTTP helpers for the old OpenID client. |
| lib/src/openid/src/model.dart | Removes embedded model aggregator for old OpenID client. |
| lib/src/openid/src/model/metadata.dart | Removes embedded provider metadata model. |
| lib/src/openid/src/model/token_response.dart | Removes embedded token response model. |
| lib/src/openid/src/model/token.dart | Removes embedded id_token wrapper. |
| lib/src/openid/src/model/claims.dart | Removes embedded claims/userinfo model. |
| lib/src/auth_manager/auth_manager_abstract.dart | Removes old web/mobile auth manager abstraction. |
| lib/src/auth_manager/auth_manager_stub.dart | Removes old auth manager stub. |
| lib/src/auth_manager/web_auth_manager.dart | Removes old web auth manager implementation. |
| test/dpop_credentials_test.dart | Adds tests for credential serialization and DPoP generation API surface. |
| test/solid_oidc_user_manager_test.dart | Adds tests for prompt/scope calculation behavior in the new manager. |
| doc/dpop_worker_threads.md | Adds documentation for worker/isolate credential export and security model. |
| README.md | Updates README to new OIDC-based API and documents worker thread approach. |
| example/lib/main.dart | Refactors example app to use SolidAuth + reactive auth state + init flow. |
| example/lib/screens/LoginScreen.dart | Updates login flow to call SolidAuth.authenticate() and remove legacy auth. |
| example/lib/screens/PrivateScreen.dart | Refactors to pass SolidAuth instead of legacy auth data. |
| example/lib/screens/PrivateProfile.dart | Updates private profile fetch to use DPoP from SolidAuth.genDpopToken(). |
| example/lib/screens/PublicScreen.dart | Passes SolidAuth into public profile route. |
| example/lib/screens/PublicProfile.dart | Refactors public profile fetch + passes SolidAuth through widgets. |
| example/lib/screens/ProfileInfo.dart | Removes legacy authData/webId plumbing; uses SolidAuth for navigation needs. |
| example/lib/screens/EditProfile.dart | Refactors updates to use SolidAuth + new Solid API helper. |
| example/lib/components/Header.dart | Updates header to use solidAuth.logout() and auth state notifier. |
| example/lib/models/SolidApi.dart | Updates PATCH/update calls to generate DPoP via SolidAuth. |
| example/lib/models/GetRdfData.dart | Formatting refactor in RDF parsing helper. |
| example/lib/dpop_worker_example.dart | Adds an isolate-based DPoP generation example for the new worker API. |
| example/redirect.html | Adds OIDC redirect handler page compatible with the oidc web flow. |
| example/web/callback.html | Removes legacy callback page. |
| example/client-profile.jsonld | Adds example Solid OIDC client profile document for hosted configuration. |
| example/pubspec.yaml | Simplifies example deps to rely on solid_auth rather than duplicating internals. |
| example/pubspec.lock | Updates lockfile to new dependency graph after OIDC migration. |
| example/.gitignore | Adds build/tooling folders to ignore list. |
| .github/workflows/deploy-web.yml | Adds GitHub Pages workflow for deploying the example web app. |
Comments suppressed due to low confidence (1)
pubspec.yaml:8
environment.sdkis still>=2.16.1, but the PR pinsoidc: ^0.14.0+2/oidc_default_store: ^0.6.0+2, which require a much newer Dart SDK. This will causepub getto fail; bump the SDK (and likely Flutter) constraints to match the minimum required by these dependencies.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # FIXME: why was pointycastle downgraded to ^3.9.1 before? | ||
| pointycastle: '>=3.9.1 <5.0.0' | ||
| uuid: ^4.5.1 | ||
| oidc: ^0.14.0+2 | ||
| oidc_default_store: ^0.6.0+2 |
There was a problem hiding this comment.
There are leftover FIXME notes and trailing whitespace in published metadata (oidc_default_store: ^0.6.0+2 ). Please remove the FIXME comment and the trailing spaces before release/publishing to avoid noisy diffs and lint/style issues.
| "htu": endPointUrl, | ||
| "htm": httpMethod, | ||
| "jti": tokenId, | ||
| "iat": (DateTime.now().millisecondsSinceEpoch / 1000).round() |
There was a problem hiding this comment.
iat is computed using .round(), which can produce a timestamp up to ~0.5s in the future. Some servers validate iat strictly and may reject proofs with future timestamps. Prefer integer division / floor() (and consider DateTime.now().toUtc()) so iat never exceeds the current second.
| "iat": (DateTime.now().millisecondsSinceEpoch / 1000).round() | |
| "iat": DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 |
| /// Public key in JSON Web Key (JWK) format | ||
| final Map<String, dynamic> publicKeyJwk; | ||
|
|
||
| /// OAuth2 access token for the authenticated user | ||
| /// | ||
| /// **Warning**: This is a bearer token that grants access to resources. | ||
| final String accessToken; | ||
|
|
||
| const DpopCredentials({ | ||
| required this.publicKey, | ||
| required this.privateKey, | ||
| required this.publicKeyJwk, | ||
| required this.accessToken, | ||
| }); |
There was a problem hiding this comment.
DpopCredentials is documented as immutable, but publicKeyJwk is stored as a mutable Map and is exposed directly. Consider defensively copying + wrapping it (e.g., Map.unmodifiable(Map<String, dynamic>.from(...))) in the constructor/fromJson to make the immutability guarantee real and avoid accidental mutation across threads.
| padding: const EdgeInsets.all(kDefaultPadding / 1.5), | ||
| child: Row( | ||
| children: [ | ||
| if (Responsive.isMobile(context) & (isAuthenticated)) |
There was a problem hiding this comment.
Bitwise & is used here instead of logical &&. Use && for boolean conditions to avoid non-short-circuit evaluation and improve readability.
| if (Responsive.isMobile(context) & (isAuthenticated)) | |
| if (Responsive.isMobile(context) && isAuthenticated) |
| onPressed: () { | ||
| // Logout and let reactive main app handle screen transition | ||
| solidAuth.logout(); |
There was a problem hiding this comment.
solidAuth.logout() returns a Future, but it's called without await/unawaited. With lints/core.yaml, this typically triggers unawaited_futures, and it can also cause the UI to transition before logout cleanup finishes. Make the handler async and await solidAuth.logout() (or explicitly wrap with unawaited(...)).
| onPressed: () { | |
| // Logout and let reactive main app handle screen transition | |
| solidAuth.logout(); | |
| onPressed: () async { | |
| // Logout and let reactive main app handle screen transition | |
| await solidAuth.logout(); |
| DPoP genDpopToken(String url, String method) { | ||
| return _manager!.genDpopToken(url, method); | ||
| } |
There was a problem hiding this comment.
genDpopToken force-unwraps _manager (_manager!) without checking authentication state. If called before init()/authenticate() (or after logout()), this throws a null-check error instead of the documented exception. Add an explicit guard (e.g., if (!isAuthenticated) throw ...) so callers get a clear, stable error.
| if (_manager == null) { | ||
| throw Exception('SolidAuth not initialized. Call authenticate() first.'); | ||
| } |
There was a problem hiding this comment.
The thrown message says "SolidAuth not initialized. Call authenticate() first." but _manager == null can also mean init() was called but no session exists (or session restoration failed). Consider rewording to something like "No active session. Call init() and authenticate() first." to make the error actionable.
| } | ||
| } | ||
| if(pictureUrl.isEmpty & optionalPictureUrl.isNotEmpty){ | ||
| if (pictureUrl.isEmpty & optionalPictureUrl.isNotEmpty) { |
There was a problem hiding this comment.
This uses bitwise & instead of logical &&. With booleans, & always evaluates both sides and is easy to misuse; use && here to express intent and keep consistent short-circuiting semantics.
| if (pictureUrl.isEmpty & optionalPictureUrl.isNotEmpty) { | |
| if (pictureUrl.isEmpty && optionalPictureUrl.isNotEmpty) { |
| @override | ||
| void dispose() { | ||
| // Properly dispose of SolidAuth resources when the app shuts down | ||
| solidAuth.dispose(); | ||
| super.dispose(); |
There was a problem hiding this comment.
SolidAuth.dispose() is async but is called without await/unawaited inside State.dispose(). With lints/core.yaml this can raise an unawaited_futures lint, and it may leave resources undisposed. Use unawaited(solidAuth.dispose()) (from dart:async) or restructure disposal so the Future is handled explicitly.
| push: | ||
| # FIXME: Change to [ main ] when merging to upstream - this is currently set for fork development | ||
| branches: [ feat/migrate-to-bdaya-oidc-security-fix ] | ||
| # FIXME: Add back pull_request trigger when merging to upstream: |
There was a problem hiding this comment.
This workflow is currently hardcoded to run only on the feat/migrate-to-bdaya-oidc-security-fix branch (and the deploy job checks the same ref). If this is intended for upstream/mainline usage, switch triggers/conditions to main (or make them configurable) before merging; otherwise the workflow will never run for the default branch.
🚀 Overview
Adds worker thread support for DPoP token generation, enabling better performance by offloading cryptographic operations from the main thread to Dart isolates or web workers.
Dependencies
Please review and merge #24 (
feat/migrate-to-bdaya-oidc-security-fix) first, as this PR builds on top of those changes.The diff will look large because it includes commits from the base PR, but only the last commit (
feat: add worker thread support) is new.✨ What's New
API
Key Features
DpopCredentialsclass: Immutable, serializable container for RSA keys and access tokensexportDpopCredentials(): Export method onSolidAuthandSolidOidcUserManagergenerateDpopToken(): Instance method for thread-safe token generation🔒 Security
Credentials transfer is safe because:
📚 Files Added
doc/dpop_worker_threads.md: Complete security model, API reference, usage examplesexample/dpop_worker_example.dart: Working isolate example with error handlingtest/dpop_credentials_test.dart: 7 tests covering serialization, immutability, generation🎯 When to Use
Worker threads: Many concurrent tokens, main thread responsiveness critical, profiling shows bottleneck
Main thread: Few tokens, simplicity preferred (sufficient for most apps)
📦 Changes
lib/src/oidc/solid_oidc_user_manager.dart- AddedDpopCredentialsclass, export methodlib/src/solid_auth.dart- Added wrapper export methodNote: Advanced optimization feature. Most apps should use
solidAuth.genDpopToken()unless profiling shows DPoP generation as bottleneck.