diff --git a/.lycheeignore b/.lycheeignore index ba0d3bd..4084c44 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -94,6 +94,9 @@ https://anusii.github.io/mypod/redirect.html https://your-solid-server/ https://dev.empwr.au/mypod/ https://server/alice/ +https://anushkavidanage.github.io/solid_auth/example_app/client-profile.jsonld +https://alice.solidcommunity.net/profile/card#me +https://solidcommunity.net/ # 20260605 gjw Failing solid servers diff --git a/example/.metadata b/example/.metadata index 87ba2b3..cf4b47a 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "ff37bef603469fb030f2b72995ab929ccfc227f0" + revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020" channel: "stable" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 - base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: android + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: ios + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: linux + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: macos + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 - platform: web - create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 - base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0 + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: windows + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 # User provided section diff --git a/lib/src/auth/solid_auth_manager.dart b/lib/src/auth/solid_auth_manager.dart index 7184ed2..2145211 100644 --- a/lib/src/auth/solid_auth_manager.dart +++ b/lib/src/auth/solid_auth_manager.dart @@ -32,6 +32,7 @@ import 'package:logging/logging.dart'; import 'package:oidc/oidc.dart'; import 'package:solid_auth/src/auth/solid_auth_session_store.dart'; +import 'package:solid_auth/src/auth/solid_auth_store.dart'; import 'package:solid_auth/src/auth/solid_oidc_config.dart'; import 'package:solid_auth/src/auth/solid_oidc_manager_factory.dart'; import 'package:solid_auth/src/dpop/dpop_key_manager.dart'; @@ -170,6 +171,11 @@ class SolidAuthManager { required String issuerUri, List? scopeOverride, }) async { + // On web, remove any DPoP private key / session parameters a previous + // persistent build may have left in localStorage. No-op on native. + + await purgeLegacyWebSecrets(); + await initForIssuer( issuerUri, scopeOverride: scopeOverride, diff --git a/lib/src/auth/solid_auth_session_store.dart b/lib/src/auth/solid_auth_session_store.dart index b4d375e..370011a 100644 --- a/lib/src/auth/solid_auth_session_store.dart +++ b/lib/src/auth/solid_auth_session_store.dart @@ -31,7 +31,8 @@ import 'dart:convert'; import 'package:logging/logging.dart'; import 'package:oidc_core/oidc_core.dart'; -import 'package:oidc_default_store/oidc_default_store.dart'; + +import 'package:solid_auth/src/auth/solid_auth_store.dart'; final _log = Logger('solid_auth.SolidAuthSessionStore'); @@ -85,7 +86,10 @@ class SolidAuthSessionStore { static const _privateKeyKey = 'solid_auth_rsa_private'; static const _publicKeyKey = 'solid_auth_rsa_public'; - final _store = OidcDefaultStore(); + // Platform-aware store: persistent (OS-backed) on native, in-memory on web + // so the DPoP private key is never written to localStorage (see + // [createSolidAuthStore]). + final OidcStore _store = createSolidAuthStore(); /// Persists all parameters required to restore this session later. /// @@ -96,7 +100,7 @@ class SolidAuthSessionStore { required String privateKeyPem, required String publicKeyPem, }) async { - if (!_store.didInit) await _store.init(); + await _store.init(); _log.fine('Saving session for issuer: $issuerUri'); await _store.setMany( OidcStoreNamespace.secureTokens, @@ -153,7 +157,7 @@ class SolidAuthSessionStore { /// /// Should be called on logout or when the session is no longer valid. Future clearSession() async { - if (!_store.didInit) await _store.init(); + await _store.init(); _log.fine('Clearing stored session'); await _store.removeMany( OidcStoreNamespace.secureTokens, diff --git a/lib/src/auth/solid_auth_store.dart b/lib/src/auth/solid_auth_store.dart new file mode 100644 index 0000000..f04c798 --- /dev/null +++ b/lib/src/auth/solid_auth_store.dart @@ -0,0 +1,100 @@ +/// OIDC storage selection for Solid-OIDC sessions. +/// +/// Copyright (C) 2026, Software Innovation Institute, ANU. +/// +/// Licensed under the MIT License (the "License"). +/// +/// License: https://choosealicense.com/licenses/mit/. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +/// +/// Authors: Tony Chen + +library; + +import 'package:logging/logging.dart'; +import 'package:oidc/oidc.dart'; +import 'package:oidc_default_store/oidc_default_store.dart'; + +final _log = Logger('solid_auth.SolidAuthStore'); + +// Pure-Dart web detection (this package does not depend on Flutter, so we +// cannot use `kIsWeb`). The `dart.library.js_interop` environment flag is set +// by the compiler on web targets (JS and WASM) and absent on native. + +const bool _kIsWeb = bool.fromEnvironment('dart.library.js_interop'); + +// A single shared in-memory store used for the whole web session, so the OIDC +// manager and the session store operate on the same data within a session. + +final OidcStore _webMemoryStore = OidcMemoryStore(); + +// The persistent session-store keys a previous (persistent) build wrote to web +// localStorage; used only to purge them from upgrading clients. + +const Set _legacyWebSessionKeys = { + 'solid_auth_issuer_uri', + 'solid_auth_scopes', + 'solid_auth_rsa_private', + 'solid_auth_rsa_public', +}; + +/// Returns the OIDC store used for DPoP keys, tokens and session state. +/// +/// On the **web** platform this is a single shared in-memory store, so the DPoP +/// private key and the OIDC tokens are NEVER written to `localStorage`. On web, +/// `flutter_secure_storage` keeps its AES-GCM key unwrapped in the same +/// `localStorage` as the ciphertext, so persisting these secrets there would +/// let any same-origin script (XSS) or a storage snapshot recover both the +/// tokens and the DPoP private key — defeating DPoP entirely. The trade-off is +/// that a web session does not survive a page reload: the user re-authenticates +/// and a fresh DPoP key pair is generated. +/// +/// Native platforms keep the persistent, OS-backed store (`OidcDefaultStore`), +/// so their sessions are restored across app restarts exactly as before. + +OidcStore createSolidAuthStore() => + _kIsWeb ? _webMemoryStore : OidcDefaultStore(); + +/// Remove any DPoP private key / session parameters a previous *persistent* +/// build left in web `localStorage`. +/// +/// New sessions never write these on web (see [createSolidAuthStore]), but an +/// upgrading client may still have the old, exposed values on disk. This is a +/// best-effort, web-only cleanup; a no-op on native platforms. Short-lived OIDC +/// token entries are left to expire rather than enumerated here. + +Future purgeLegacyWebSecrets() async { + if (!_kIsWeb) { + return; + } + try { + final persistent = OidcDefaultStore(); + await persistent.init(); + await persistent.removeMany( + OidcStoreNamespace.secureTokens, + keys: _legacyWebSessionKeys, + ); + _log.fine('Purged legacy web session secrets from persistent storage'); + } on Object catch (e) { + // Never fail login because of a best-effort cleanup. + + _log.fine('purgeLegacyWebSecrets() skipped: ${e.runtimeType}'); + } +} diff --git a/lib/src/auth/solid_oidc_manager_factory.dart b/lib/src/auth/solid_oidc_manager_factory.dart index caabf6e..d61a4e2 100644 --- a/lib/src/auth/solid_oidc_manager_factory.dart +++ b/lib/src/auth/solid_oidc_manager_factory.dart @@ -29,8 +29,8 @@ library; import 'package:logging/logging.dart'; import 'package:oidc/oidc.dart'; -import 'package:oidc_default_store/oidc_default_store.dart'; +import 'package:solid_auth/src/auth/solid_auth_store.dart'; import 'package:solid_auth/src/auth/solid_oidc_config.dart'; import 'package:solid_auth/src/dpop/dpop_key_manager.dart'; import 'package:solid_auth/src/dpop/dpop_token_generator.dart'; @@ -174,7 +174,7 @@ abstract class SolidOidcManagerFactory { ? OidcUserManager( discoveryDocument: metadata.oidcMetadata, clientCredentials: clientAuth, - store: OidcDefaultStore(), + store: createSolidAuthStore(), settings: settings, httpClient: config.httpClient, keyStore: null, @@ -185,7 +185,7 @@ abstract class SolidOidcManagerFactory { Uri.parse(issuerUri), ), clientCredentials: clientAuth, - store: OidcDefaultStore(), + store: createSolidAuthStore(), settings: settings, httpClient: config.httpClient, keyStore: null,