Skip to content

Feat/dpop worker thread support#33

Open
kkalass wants to merge 14 commits intoanusii:devfrom
kkalass:feat/dpop-worker-thread-support
Open

Feat/dpop worker thread support#33
kkalass wants to merge 14 commits intoanusii:devfrom
kkalass:feat/dpop-worker-thread-support

Conversation

@kkalass
Copy link
Copy Markdown

@kkalass kkalass commented Oct 29, 2025

🚀 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

⚠️ This PR depends on #24
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

// Export credentials for worker thread
final credentials = solidAuth.exportDpopCredentials();

// Generate DPoP token in isolate
void workerFunction(Map<String, dynamic> json) {
  final credentials = DpopCredentials.fromJson(json);
  final dpop = credentials.generateDpopToken(url: url, method: 'GET');
}

await Isolate.spawn(workerFunction, credentials.toJson());

Key Features

  • DpopCredentials class: Immutable, serializable container for RSA keys and access tokens
  • exportDpopCredentials(): Export method on SolidAuth and SolidOidcUserManager
  • generateDpopToken(): Instance method for thread-safe token generation
  • Security model: Clear intra-process (✅ safe) vs. external transfer (❌ unsafe) documentation

🔒 Security

Credentials transfer is safe because:

  • Stays within OS-protected process boundary
  • Standard practice in multi-threaded crypto (OpenSSL, BoringSSL)
  • Comprehensive docs distinguish safe (isolates, compute()) from unsafe (network, storage) usage

📚 Files Added

  • doc/dpop_worker_threads.md: Complete security model, API reference, usage examples
  • example/dpop_worker_example.dart: Working isolate example with error handling
  • test/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

  • Modified: lib/src/oidc/solid_oidc_user_manager.dart - Added DpopCredentials class, export method
  • Modified: lib/src/solid_auth.dart - Added wrapper export method
  • Added: Documentation, example, tests (~425 lines total)

Note: Advanced optimization feature. Most apps should use solidAuth.genDpopToken() unless profiling shows DPoP generation as bottleneck.

kkalass and others added 13 commits August 3, 2025 20:57
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
Copilot AI review requested due to automatic review settings April 16, 2026 07:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.dart entrypoint 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.sdk is still >=2.16.1, but the PR pins oidc: ^0.14.0+2 / oidc_default_store: ^0.6.0+2, which require a much newer Dart SDK. This will cause pub get to 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.

Comment thread pubspec.yaml
Comment on lines +20 to +24
# 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
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
"htu": endPointUrl,
"htm": httpMethod,
"jti": tokenId,
"iat": (DateTime.now().millisecondsSinceEpoch / 1000).round()
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
"iat": (DateTime.now().millisecondsSinceEpoch / 1000).round()
"iat": DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +65
/// 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,
});
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
padding: const EdgeInsets.all(kDefaultPadding / 1.5),
child: Row(
children: [
if (Responsive.isMobile(context) & (isAuthenticated))
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Bitwise & is used here instead of logical &&. Use && for boolean conditions to avoid non-short-circuit evaluation and improve readability.

Suggested change
if (Responsive.isMobile(context) & (isAuthenticated))
if (Responsive.isMobile(context) && isAuthenticated)

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +56
onPressed: () {
// Logout and let reactive main app handle screen transition
solidAuth.logout();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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(...)).

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment thread lib/src/solid_auth.dart
Comment on lines +969 to +971
DPoP genDpopToken(String url, String method) {
return _manager!.genDpopToken(url, method);
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment thread lib/src/solid_auth.dart
Comment on lines +1034 to +1036
if (_manager == null) {
throw Exception('SolidAuth not initialized. Call authenticate() first.');
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
}
}
if(pictureUrl.isEmpty & optionalPictureUrl.isNotEmpty){
if (pictureUrl.isEmpty & optionalPictureUrl.isNotEmpty) {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
if (pictureUrl.isEmpty & optionalPictureUrl.isNotEmpty) {
if (pictureUrl.isEmpty && optionalPictureUrl.isNotEmpty) {

Copilot uses AI. Check for mistakes.
Comment thread example/lib/main.dart
Comment on lines +108 to +112
@override
void dispose() {
// Properly dispose of SolidAuth resources when the app shuts down
solidAuth.dispose();
super.dispose();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +7
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:
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants