Skip to content

anusii/solidpod

Repository files navigation

Flutter Dart

GitHub License GitHub Version Pub Version GitHub Last Updated GitHub Commit Activity (main) GitHub Issues

Solid Pod

A package to support access to your Data Vault hosted on a Solid Server, implemented by the ANU Software Innovation Institute supporting the Australian Solid Community.

Authors: Anushka Vidanage, Graham Williams, Jessica Moore, Dawei Chen, Kevin Wang, Zheyuan Xu.

Free (as in Libre) and Open Source Software License: MIT

See the AU Solid Community page for apps utilising the solidpod package.

Introduction

SolidPod provides the core business logic for Dart applications to manage personal online data stores (PODs) hosted in a Data Vault on a Solid Server. It supports authenticating users to their PODs, reading and writing data, and managing access permissions programmatically. The companion solidui package builds on top of SolidPod to provide ready-made Flutter widgets for login screens, permission management, and other user-facing features.

What is Solid?

Solid (https://solidproject.org/) is an open standard for a server to host personal online data stores (Pods). Numerous providers of Solid Server hosting are emerging allowing users to host and migrate their Pods on any such servers (or to run their own server).

To know more about our work related to Solid Pods visit https://solidcommunity.au

Features

For UI components such as login screens, security key management, permission granting/revoking, and shared resource views, see the solidui package.

hosts support users host and migrate their Pods. Anyone can also host their own Community Solid Server. To know more about our work visit the ANU's Software Innovation Institute and the Australian Solid Community.

Getting started

To start using the package add solidpod as a dependency in your pubspec.yaml file.

dependencies:
  solidpod: ^<latest-version>

An example project that uses solidpod can be found in the example folder of the solidpod repository.

Prerequisites

If the package is being used to build either a macos or web app, the following changes are required in order to make the package fully functional.

General

solidpod delegates authentication to package:solid_auth, which is built on the OpenID-certified package:oidc and implements the Solid-OIDC protocol.

Authentication requires a client ID document, which is a publicly hosted JSON-LD file that identifies your app to the Solid identity provider. Pass its URL as the clientId parameter to solidAuthenticate(). See the Solid-OIDC client identifiers spec for how to create and host one. For an example client ID document refer to the client_profile.jsonld.

Android

As per OIDC getting started guide update the following.

Go to android/app/build.gradle, and add the following line under defaultConfig:

 defaultConfig {
    ...
    manifestPlaceholders += [
    'appAuthRedirectScheme': 'com.my.app'
    ]
}

Replace com.my.app with your applicationId. If you have a build.gradle.kts file upgrade in the following way

 defaultConfig {
    ...
    manifestPlaceholders.putAll(mapOf(
            "appAuthRedirectScheme" to "com.my.app"
        ))
}

Go to android/app/src/main/AndroidManifest.xml, and add the following under application tag:

<application
        ...
        android:fullBackupContent="@xml/backup_rules"
        android:dataExtractionRules="@xml/data_extraction_rules"
        >

Also under activity tab change the following:

  • Remove the line android:taskAffinity=""
  • Change android:launchMode="singleTop" to android:launchMode="singleTask"

Now create the following file in android\app\src\main\res\xml\backup_rules.xml

<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
    <exclude domain="sharedpref" path="FlutterSecureStorage"/>
</full-backup-content>

Also create the following file in android\app\src\main\res\xml\data_extraction_rules.xml

<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
    <cloud-backup>
        <exclude domain="sharedpref" path="FlutterSecureStorage"/>
    </cloud-backup>
</data-extraction-rules>

For a release be sure to update android/app/src/main/AndroidManifest.xml to include within the queries section of the manifest:

 <!-- If your app opens https URLs -->
<intent>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="https"/>
</intent>

macos

Inside the app directory go to the directory /macos/Runner/. Inside there are two files named DebugProfile.entitlements and Release.entitlements. Add the following lines inside the <dict> </dict> tag in both files.

<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>keychain-access-groups</key>
<array/>
<key>com.apple.security.keychain</key>
<true/>

Note: You may already have some of the above lines in those files. If so fill the missing.

web

In the same location where your client ID document is hosted, create a file called redirect.html. Add the following piece of html code into that file.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>Flutter Oidc Redirect</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="text/javascript">
        const stateNamespace = 'state';
        const stateResponseNamespace = 'response.state';
        const requestNamespace = 'request';

        const requestBroadcastChannel = 'oidc_flutter_web/request';
        const redirectBroadcastChannel = 'oidc_flutter_web/redirect';


        //if the OP isn't requesting logout, handle redirect.
        if (!handleFrontChannelLogout()) {
            handleRedirect();
        }

        function handleRedirect() {
            // For supported browsers: https://caniuse.com/broadcastchannel
            var bc = new BroadcastChannel(redirectBroadcastChannel);
            bc.postMessage(window.location.toString());
            bc.close();
            //The rest of this function handles same page redirects
            let dataSrc;
            dataSrc = new URLSearchParams(window.location.search);
            var state = dataSrc.get('state');
            if (!state) {
                if (window.location.hash) {
                    dataSrc = new URLSearchParams(
                            window.location.hash.substring(1)
                    );
                    state = dataSrc.get('state');
                }
            }
            if (!state) {
                return;
            }
            const stateDataRaw = getLocalStorage(stateNamespace, state);
            if (!stateDataRaw) {
                console.error('state not found, key: ' + state);
                return;
            }
            setLocalStorage(stateResponseNamespace, state, window.location.toString());
            //we call JSON.parse twice, since shared_preferences double encodes json strings for some reason.
            const parsedStateString = JSON.parse(stateDataRaw);
            if (!parsedStateString) {
                console.error('parsed state is null');
                return;
            }
            // Read the mode from the state.
            const webLaunchMode = parsedStateString.options?.webLaunchMode;
            if (!webLaunchMode) {
                console.error('webLaunchMode not found in parsed state.');
                return;
            }
            if (webLaunchMode != 'samePage') {
                return;
            }
            const original_uri = parsedStateString.original_uri;
            if (!original_uri) {
                console.warn("it's preferred that original_uri is used when webLaunchMode is samePage.");
                return;
            }
            window.location.assign(original_uri);
        }

        function handleFrontChannelLogout() {
            const queryParams = new URLSearchParams(window.location.search);
            if (queryParams.get('requestType') == 'front-channel-logout') {
                // For supported browsers: https://caniuse.com/broadcastchannel
                var bc = new BroadcastChannel(requestBroadcastChannel);
                bc.postMessage(window.location.toString());
                bc.close();
                // this puts a marker for the flutter app that the user wants to logout.
                //
                // in the flutter app, if this marker exists,
                // we don't auth the cached user in `UserManager.init()`, and we clear the cached data.
                setLocalStorage(requestNamespace, 'front-channel-logout', window.location.toString());
                return true;
            }
            return false;
        }

        function getLocalStorage(namespace, key) {
            const rawRes = localStorage.getItem('oidc.' + namespace + '.' + key);
            if (!rawRes) {
                return null;
            }
            return rawRes;
        }

        function setLocalStorage(namespace, key, value) {
            const keysEntryKey = 'oidc.keys.' + namespace;
            var keys = localStorage.getItem(keysEntryKey);
            if (!keys) {
                keys = "[]";
            }
            const parsedKeys = JSON.parse(keys);
            if (!(parsedKeys instanceof Array)) {
                console.error('parsedKeys is not an array.', parsedKeys);
            }
            parsedKeys.push(key);
            localStorage.setItem(keysEntryKey, JSON.stringify(parsedKeys));
            localStorage.setItem('oidc.' + namespace + '.' + key, value);
        }
    </script>
</head>

<body>
<h2>Authentication completed! Please close this page.</h2>
</body>

</html>

Usage

Following are the usage of main functionalities supported by the package.

Authenticate Example

Authenticates a user against a Solid server. The first argument can be either the user's WebID (preferred) or a bare issuer URI. Returns [SolidAuthData, webId, profileTurtle] on success, or null on failure.

final result = await
solidAuthenticate
('https://pods.solidcommunity.au/alice/profile/card#me
'
, // WebID or issuer URI
context,
clientId: 'https://your-domain/client-profile.jsonld',
redirectUris: [
'https://your-domain/redirect.html', // web
'com.example.app://redirect', // Android / iOS
'http://localhost:4400/redirect', // Windows / Linux / macOS
],
postLogoutRedirectUris: [ // optional, defaults to redirectUris selection
'https://your-domain/redirect.html',
'com.example.app://redirect',
'http://localhost:4400/redirect',
],
);

if (result != null) {
final authData = result[0] as SolidAuthData; // access token, id token, etc.
final webId = result[1] as String; // user's WebID
final profile = result[2] as String; // Turtle-encoded profile document
}

IMPORTANT

redirectUris and postLogoutRedirectUris take a list of URIs, one per platform. At runtime solidAuthenticate() picks the entry that matches the current platform. Every URI in the list must be registered in your client ID document and match the correct format for each platform:

Platform URI format Notes
Web https://your-domain/redirect.html Must be same origin as the app - oidc uses BroadcastChannel (same-origin only)
Android / iOS / macOS com.example.app://redirect Custom URI scheme registered with the OS
Windows / Linux http://localhost:4400/redirect Fixed port required - see below

Desktop: use a fixed port

oidc_desktop binds a loopback HTTP server to the port in the desktop entry of redirectUris. If you use port 0, the OS assigns a random port that is never registered in the client document, causing the Solid server to reject logout with post_logout_redirect_uri not registered. Use a fixed port (e.g. 4400) in both the app and the client document.

Both redirect_uris and post_logout_redirect_uris in the client ID document must list every URI used across platforms:

{
  "redirect_uris": [
    "https://your-domain/redirect.html",
    "http://localhost:4400/redirect"
  ],
  "post_logout_redirect_uris": [
    "https://your-domain/redirect.html",
    "http://localhost:4400/redirect"
  ]
}

Session Restore Example

On app startup, call tryRestoreSession() to silently resume a previous session without opening a browser. It returns [SolidAuthData, webId, profileTurtle] if a valid persisted session exists, or null if the user needs to log in.

// In initState() or app startup code — before showing the login UI.
final result = await

tryRestoreSession();if (
result != null) {
final authData = result[0] as SolidAuthData;
final webId = result[1] as String;
final profile = result[2] as String;
// Navigate directly to the authenticated screen.
} else {
// No valid session — show the login screen.
}

tryRestoreSession() never opens a browser. It silently refreshes expired access tokens if a refresh token is available, and returns null if the session cannot be restored (in which case the stored session is cleared automatically so the next solidAuthenticate() starts clean).

Read Pod File Example

Read data from the file data/myfiles/my-data-file.ttl.

final fileContent = await
readPod
('data/myfiles/my-data-file.ttl
'
,
);

Write to Pod File Example

Write data to the file myfiles/my-data-file.ttl.

// Turtle string to be written to the file
final turtleString =
    '@prefix somePrefix: <http://www.perceive.net/schemas/relationship/> .
        < http: //example.org/#green-goblin> somePrefix:enemyOf
<
http: //example.org/#spiderman> .';

await writePod
('myfiles/my-data-file.ttl
'
,turtleString,
encrypted
:
false // non-required parameter. By default set to true
);

writePod() also supports using inherited encryption keys and .acl files. For instance, consider the following use-case.

Use-case: Write two files parentDir/child-1.ttl and parentDir/child-1.ttl into a single directory parentDir. Use a single .acl file for both the files and use a single encryption key to encrypt both the files.

Above can be achieved using following lines of code.

// Turtle string to be written to the file
final childDataString = '<Sample TTL Data>';

await writePod
('parentDir/child-1.ttl
'
,childDataString,
createAcl: false,
inheritKeyFrom: 'parentDir/',
);

await writePod(
'parentDir/child-2.ttl',
childDataString,
createAcl: false,
inheritKeyFrom: 'parentDir/'
,
);

The above will create a single .acl file for the directory parentDir and use that as .acl file for both child-1.ttl and child-2.ttl files. Also it will create a single key associated with the directory parentDir and encrypt both files using that key.

Delete a File from the Pod

// Obtain the full URL for the file first.
final fileUrl = await
getFileUrl
('myfiles/my-data-file.ttl
'
);

// Delete the file, its ACL, and its encryption key (if any).
// Also revokes any permissions previously granted to other users.
await deleteFile
(
fileUrl
:
fileUrl
);

To delete an entire directory and all of its contents recursively:

await deleteContainer
('myapp/data
'
,
'
myfiles
'
);

Large File Manager Example

To upload a large file in application myapp, use:

await writeLargeFile
(
// Name of the file in POD
remoteFilePath: 'my-large-file.bin',
// Path of the file where it is locally stored
localFilePath: 'D:/my-large-file.bin',
)

The uploaded file will be stored in the myapp/data folder.

To download a large file use:

await readLargeFile
(
// Name of the file in POD
remoteFilePath: 'my-large-file.bin',
// Path of the file where it will be locally downloaded
localFilePath: 'D:/my-large-file.bin',
)

To delete a large file use:

await deleteLargeFile
(
// Name of the file in POD
remoteFilePath
:
'
my-large-file.bin
'
,
)

Ontology

A Solid Pod's internal storage structure consists of turtle files containing security information about the pod's content (data files) and access. The internal structure is based on the solidpod ontology, which captures essential concepts about the app's security information, data files, encryption, shared resources, and access control lists.

Ontolgy

Additional information

The source code can be accessed via the GitHub repository. You can also file issues at GitHub Issues. The authors of the package will respond to issues as best we can but.

Time-stamp: <Wednesday 2026-06-03 16:48:48 +1000 Graham Williams>

About

No description, website, or topics provided.

Resources

License

MIT, Unknown licenses found

Licenses found

MIT
LICENSE
Unknown
license.dart

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors