Skip to content
Open
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,10 @@ app.*.symbols
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock

#-----------------------------------------------------------------------
# Claude
#-----------------------------------------------------------------------

CLAUDE.md
.claude/*
12 changes: 6 additions & 6 deletions example/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.

version:
revision: "2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa"
revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
channel: "stable"

project_type: app
Expand All @@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
- platform: linux
create_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
base_revision: 2c9eb20739dfec95e2c74bd3dfa4601b0a8a36aa
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: windows
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0

# User provided section

Expand Down
10 changes: 10 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ class DemoPod extends StatelessWidget {
infoButtonStyle: InfoButtonStyle(
tooltip: 'Visit the DemoPod documentation.',
),
clientId:
'https://anushkavidanage.github.io/solidpod/example/client-profile.jsonld',
// Use the following schemas depending on the platform
// Web: https://anushkavidanage.github.io/solidpod/example/redirect.html
// Mobile: com.example.demopod://redirect
// Desktop: http://localhost:4400/redirect
// (can use any port as long as it matches with the one in your id document)
redirectUri: 'com.example.demopod://redirect',
postLogoutRedirectUri: 'com.example.demopod://redirect',
autoLogin: true,
child: Home(),
),
);
Expand Down
4 changes: 4 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ dependencies:
dependency_overrides:
solidpod:
path: ..
solidui:
git:
url: https://github.com/anusii/solidui.git
ref: av/40_migrate_oidc_implementation

dev_dependencies:
dependency_validator: ^5.0.4
Expand Down
30 changes: 30 additions & 0 deletions example/test/widget_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.

import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';

// import 'package:demopod/main.dart';

// void main() {
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// // Build our app and trigger a frame.
// await tester.pumpWidget(const MyApp());

// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);

// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();

// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
// });
// }
2 changes: 1 addition & 1 deletion lib/solidpod.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export 'src/solid/constants/predicates.dart';

/// Solid authentication function

export 'src/solid/authenticate.dart' show solidAuthenticate;
export 'src/solid/authenticate.dart' show solidAuthenticate, tryRestoreSession;

/// Status class to represent different function outputs

Expand Down
181 changes: 101 additions & 80 deletions lib/src/solid/authenticate.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/// Authenticate against a solid server and return null if authentication fails.
/// Authenticate against a Solid server using Solid-OIDC.
///
// Time-stamp: <Monday 2025-07-14 11:29:39 +1000 Graham Williams>
///
Expand Down Expand Up @@ -26,112 +26,133 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
///
/// Authors: Zheyuan Xu, Graham Williams

// ignore_for_file: use_build_context_synchronously
/// Authors: Zheyuan Xu, Graham Williams, Anushka Vidanage

library;

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show debugPrint;
import 'package:flutter/material.dart' show BuildContext;

import 'package:solid_auth/solid_auth.dart';
import 'package:solid_auth/solid_auth.dart'
show SolidAuthManager, SolidOidcConfig;

import 'package:solidpod/src/solid/api/rest_api.dart';
import 'package:solidpod/src/solid/utils/authdata_manager.dart'
show AuthDataManager;
import 'package:solidpod/src/solid/utils/misc.dart'
show isUserLoggedIn, logoutPod;

// Scopes variables used in the authentication process.

final List<String> _scopes = <String>[
'openid',
'profile',
'offline_access',
'webid', // web ID is necessary to get refresh token
];
import 'package:solidpod/src/solid/utils/misc.dart' show isUserLoggedIn;

/// Asynchronously authenticate a user against a Solid server [serverId].
///
/// [serverId] is an issuer URI and is essential for the
/// authentication process with the POD (Personal Online Datastore) issuer.
/// [serverId] is the user's WebID or an issuer URI. Issuer resolution is
/// handled internally by [SolidAuthManager].
///
/// [context] of the current widget is required for the authenticate process.
/// [context] is kept for API compatibility but is no longer required by the
/// underlying Solid-OIDC flow.
///
/// Return a list containing authentication data: user's webId; profile data.
/// [clientId] must be the URL of the app's client profile JSON-LD
/// document (or a pre-registered client ID). Required.
///
/// Error Handling: The function has a catch all to return null if any exception
/// occurs during the authentication process.

/// [redirectUri] is the custom URL scheme for the OAuth to redirect to
/// after authentication.
///
/// [postLogoutRedirectUri] is an optional redirect URI for logout. If not
/// set assign the same value as [redirectUri].
///
/// Returns `[SolidAuthData, webId, profileTurtle]` on success, null on failure.
Future<List<dynamic>?> solidAuthenticate(
String serverId,
BuildContext context,
) async {
BuildContext context, {
required String clientId,
required String redirectUri,
String? postLogoutRedirectUri,
}) async {
try {
final loggedIn = await isUserLoggedIn();
Map<dynamic, dynamic>? authData;
if (loggedIn) {
authData = await AuthDataManager.loadAuthData();
if (authData == null) {
// Fall through to re-authenticate
// Return existing session without re-authenticating.
if (await isUserLoggedIn()) {
final authData = await AuthDataManager.loadAuthData();
if (authData != null) {
final profData = utf8.decode(
await getResource(authData.webId.replaceAll('#me', '')),
);
return [authData, authData.webId, profData];
}
}

// If not logged in or load failed, perform new authentication
if (!loggedIn || authData == null) {
debugPrint('solidAuthenticate() => solid_auth.authenticate($serverId)');
// Authentication process for the POD issuer.

final issuerUri = await getIssuer(serverId);
authData = await authenticate(Uri.parse(issuerUri), _scopes, context);

// Validate authentication response before saving
if (authData.isEmpty) {
return null;
}

if (authData.containsKey('error')) {
return null;
}

// Validate that required authentication fields are present
if (!authData.containsKey('accessToken') ||
authData['accessToken'] == null) {
return null;
}

// Let saveAuthData() decode the JWT and extract webId
// If webId extraction fails, saveAuthData() will handle it and skip saving
await AuthDataManager.saveAuthData(authData);

// Verify that webId was successfully extracted and saved
final webId = await AuthDataManager.getWebId();
if (webId == null || webId.isEmpty) {
return null;
}

// Proceed to fetch profile data with the authenticated credentials
final profCardUrl = webId.replaceAll('#me', '');
final profData = utf8.decode(await getResource(profCardUrl));

return [authData, webId, profData];
if (clientId.isEmpty) {
throw Exception(
'oidcClientId is required for Solid-OIDC authentication. '
'Provide the URL of your app\'s client profile JSON-LD document.',
);
}

// Already logged in successfully - fetch profile data
final webId = await AuthDataManager.getWebId();
if (webId == null || webId.isEmpty) {
await logoutPod();
return null;
}
// Check if post logout redirect URI is set. If not use the
// same value of redirect URI.
postLogoutRedirectUri ??= redirectUri;

final authManager = SolidAuthManager(
config: SolidOidcConfig(
clientId: clientId,
redirectUri: Uri.parse(redirectUri),
postLogoutRedirectUri: Uri.parse(postLogoutRedirectUri),
),
);

final solidAuthData = await authManager.authenticate(serverId);
if (solidAuthData == null) return null;

await AuthDataManager.saveAuthData(
solidAuthData,
authManager,
oidcClientId: clientId,
redirectUri: redirectUri.toString(),
);

final profData = utf8.decode(
await getResource(solidAuthData.webId.replaceAll('#me', '')),
);

return [solidAuthData, solidAuthData.webId, profData];
} on Object catch (e) {
debugPrint('Solid Authenticate Failed: $e');
return null;
}
}

final profCardUrl = webId.replaceAll('#me', '');
final profData = utf8.decode(await getResource(profCardUrl));
/// Silently restores a previously saved login session without browser interaction.
///
/// Returns `[SolidAuthData, webId, profileTurtle]` if a valid persisted session
/// is found, or `null` if there is no session or it cannot be restored.
///
/// Unlike [solidAuthenticate], this never opens a browser window. Use it on
/// app startup to automatically skip the login page when the user is still
/// logged in.
Future<List<dynamic>?> tryRestoreSession() async {
try {
final authData = await AuthDataManager.loadAuthData();
if (authData == null) return null;

return [authData, webId, profData];
final profData = utf8.decode(
await getResource(authData.webId.replaceAll('#me', '')),
);
return [authData, authData.webId, profData];
} on Object catch (e) {
debugPrint('Solid Authenticate Failed: $e');
debugPrint('tryRestoreSession failed: $e');
return null;
}
}

// /// Builds the OAuth redirect URI from [appUrlScheme] or [frontendRedirectUrl].
// Uri _buildRedirectUri(String? appUrlScheme, String? frontendRedirectUrl) {
// if (frontendRedirectUrl != null && frontendRedirectUrl.isNotEmpty) {
// return Uri.parse(frontendRedirectUrl);
// }
// if (appUrlScheme != null && appUrlScheme.isNotEmpty) {
// return Uri.parse('$appUrlScheme://callback');
// }
// throw Exception(
// 'Either appUrlScheme or frontendRedirectUrl must be provided '
// 'for the OAuth redirect.',
// );
// }
Loading
Loading