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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ description of the update. Updates in the 0.1.n series are heading
toward a 0.2 release. The `[version timestamp user]` string is
utilised by the flutter version_widget package.

## 0.3

+ Implementing Authorization Code + PKCE, DPoP key binding (RFC 9449), and WebID-based issuer discovery using OpenID-certified [`package:oidc`](https://pub.dev/packages/oidc) [0.2.0 20260521 anushkavidanage]

## 0.2 Stability

+ Update Try Another WebID workflow [0.1.30 20260520 tonypioneer]
+ Update jose dependency [0.1.29 20260415 jesscmoore]
+ Review and cleanup for publication [0.1.28 20250925 gjw]
+ Remove jwt, update openid, use encrypt_plus [0.1.28 20250923 anushkavidanage]
+ Export openid_client [0.1.28 20250917 anushkavidanage]
Expand Down
235 changes: 160 additions & 75 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,200 @@
# solid_auth (restructured)
# solid_auth

Solid-OIDC authentication for Flutter, now built on the
[OpenID-certified `oidc` package](https://pub.dev/packages/oidc).
[![Flutter](https://img.shields.io/badge/Flutter-%2302569B.svg?style=for-the-badge&logo=Flutter&logoColor=white)](https://flutter.dev)
[![Dart](https://img.shields.io/badge/dart-%230175C2.svg?style=for-the-badge&logo=dart&logoColor=white)](https://dart.dev)

[![GitHub License](https://img.shields.io/github/license/anusii/solid_auth)](https://raw.githubusercontent.com/anusii/solid_auth/dev/LICENSE)
[![GitHub Version](https://img.shields.io/badge/dynamic/yaml?url=https://raw.githubusercontent.com/anusii/solid_auth/master/pubspec.yaml&query=$.version&label=version&logo=github)](https://github.com/anusii/solid_auth/blob/dev/CHANGELOG.md)
[![Pub Version](https://img.shields.io/pub/v/solid_auth?label=pub.dev&labelColor=333940&logo=flutter)](https://pub.dev/packages/solid_auth)
[![GitHub Last Updated](https://img.shields.io/github/last-commit/anusii/solid_auth?label=last%20updated)](https://github.com/anusii/solid_auth/commits/dev/)
[![GitHub Commit Activity (main)](https://img.shields.io/github/commit-activity/w/anusii/solid_auth/dev)](https://github.com/anusii/solid_auth/commits/dev/)
[![GitHub Issues](https://img.shields.io/github/issues/anusii/solid_auth)](https://github.com/anusii/solid_auth/issues)

Solid-OIDC authentication package for Flutter apps. Handles Authorization Code + PKCE, DPoP key binding (RFC 9449), and WebID-based issuer discovery. This package is built on the OpenID-certified [`package:oidc`](https://pub.dev/packages/oidc). Implemented by the [ANU Software Innovation
Institute](https://sii.anu.edu.au) supporting the [Australian Solid
Community](https://solidcommunity.au).

---

## Architecture overview
## What is Solid?

```
solid_auth (public API)
├── SolidAuthManager ← main facade (replaces authenticate())
│ ├── loginFromWebId() ← resolves issuer, then logs in
│ ├── login() ← direct login given issuer URI
│ ├── currentAuthData ← typed SolidAuthData (not a raw Map)
│ ├── authChanges ← Stream<SolidAuthData?> (like Firebase Auth)
│ └── logout() / dispose()
├── SolidOidcManagerFactory ← wires SolidOidcConfig → OidcUserManager
│ └── create()
├── DpopTokenGenerator ← DPoP proof JWT generation (unchanged logic)
│ ├── generateForRequest() ← new: auto-fetches key from DpopKeyManager
│ └── generate() ← legacy-compatible static method
├── DpopKeyManager ← RSA key-pair lifecycle
├── ProfileFetcher ← replaces fetchProfileData()
│ └── fetchProfile() → SolidProfile
└── WebIdUtils ← replaces getIssuer()
├── getIssuer()
└── getProviderMetadata() → SolidProviderMetadata
```
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).

### Dependency map
To know more about our work related to Solid Pods
visit <https://solidcommunity.au>

```
solid_auth
└── package:oidc (OidcUserManager, OidcUserManagerSettings, etc.)
└── oidc_core (OidcProviderMetadata, OidcToken, etc.)
└── oidc_default_store (secure token persistence)
└── dart_jsonwebtoken (DPoP JWT signing — kept)
└── fast_rsa (RSA key generation — kept)
```
---

## Features

The entire forked `openid_client` code is **removed**. All OIDC discovery,
PKCE, token exchange and refresh is delegated to `package:oidc`.
- **Solid-OIDC login** via Authorization Code + PKCE on all platforms (Android, iOS, Web, Windows, macOS, Linux)
- **DPoP key binding** - generates and manages RSA-2048 key pairs; injects DPoP proof headers automatically at every token request
- **Session persistence** - saves tokens and DPoP keys to secure storage; silently restores sessions on app restart
- **WebID issuer discovery** - resolves an OIDC issuer from any WebID profile URL
- **Typed auth result** (`SolidAuthData`) - replaces the old raw `Map<String, dynamic>`

---

## Migration guide — 0.1.x → 0.2.x
## Installation

```yaml
dependencies:
solid_auth: ^0.2.0
```

<!-- If your app targets **web**, also declare the `fast_rsa` WASM worker assets in your app's `pubspec.yaml` (the package omits them from its own asset list):

| Old (0.1.x) | New (0.2.x) |
|------------------------------------------------------|----------------------------------------------------|
| `String issuer = await getIssuer(webId)` | `WebIdUtils.getIssuer(webId)` (same signature) |
| `var data = await authenticate(issuerUri, scopes)` | `SolidAuthManager.loginFromWebId(webId)` returns `SolidAuthData` |
| `data['accessToken']` | `authData.accessToken` |
| `data['idToken']` | `authData.idToken` |
| `genDpopToken(url, keyPair, jwk, method)` | `DpopTokenGenerator.generate(...)` (same params) |
| `fetchProfileData(webId)` | `ProfileFetcher().fetchProfile(webId)` |
```yaml
flutter:
assets:
- packages/fast_rsa/web/assets/worker.js
- packages/fast_rsa/web/assets/wasm_exec.js
- packages/fast_rsa/web/assets/rsa.wasm
``` -->

---

## Quick start
## Quick Start

```dart
import 'package:solid_auth/solid_auth.dart';

// 1. Create the manager (once, at app level)
// 1. Create the manager once (e.g. at widget level or in a provider).
final auth = SolidAuthManager(
config: SolidOidcConfig(
clientId: 'my_client_id',
redirectUri: Uri.parse('com.example.app://callback'),
scopes: SolidScopes.defaultScopes, // includes webid automatically
clientId: 'https://your-domain/client-profile.jsonld',
redirectUri: Uri.parse('https://your-domain/redirect.html'),
postLogoutRedirectUri: Uri.parse('https://your-domain/redirect.html'),
scopes: SolidScopes.defaultScopes, // includes `webid` automatically
),
);

// 2. Login — resolves issuer from WebID, then runs Authorization Code + PKCE
final authData = await auth.loginFromWebId(
'https://charlieb.solidcommunity.net/profile/card#me',
// 2. Login — resolves issuer from WebID, then runs Authorization Code + PKCE.
final authData = await auth.authenticate(
'https://pods.solidcommunity.au/alice-barnes/profile/card#me',
);
print(authData.webId); // https://charlieb.solidcommunity.net/profile/card#me
print(authData.webId); // https://pods.solidcommunity.au/alice-barnes/profile/card#me
print(authData.accessToken);

// 3. Generate a DPoP proof for a resource request
// 3. Generate a DPoP proof for a protected resource request.
final dpop = await DpopTokenGenerator.generateForRequest(
endpointUrl: 'https://charlieb.solidcommunity.net/private/notes.ttl',
endpointUrl: 'https://pods.solidcommunity.au/alice-barnes/notepod/data/notes.ttl',
httpMethod: 'GET',
accessToken: authData.accessToken,
keyManager: auth.keyManager, // must be the same key bound to the access token
);
// Use in HTTP headers:
// 'Authorization': 'DPoP ${authData.accessToken}'
// 'DPoP': dpop

// 4. Fetch public profile
final profile = await ProfileFetcher().fetchProfile(authData.webId);
print(profile.name);
print(profile.storage);
// 'Authorization': 'DPoP ${authData.accessToken}'
// 'DPoP': dpop

// 5. Logout
// 4. Logout.
await auth.logout();
```

---

## Platform setup
## Session Restore

After a successful login, `solid_auth` automatically saves the session (OIDC tokens + DPoP key pair) to platform-native secure storage. On the next app launch you can resume without requiring the user to log in again:

```dart
// Call this in initState before showing the login UI.
final auth = SolidAuthManager(config: SolidOidcConfig(...));

final data = await auth.tryRestoreSession();
if (data != null) {
// Valid session found — navigate directly to the authenticated screen.
print('Welcome back, ${data.webId}');
} else {
// No stored session — show the login screen.
}
```

`tryRestoreSession()` returns `null` if no session exists, if the refresh token has expired, or if any storage error occurs (in which case the stored session is cleared so the next login starts clean).

Calling `logout()` or `forgetUser()` always clears the stored session.

---

## DPoP for Resource Requests

Every request to a Solid server protected resource needs both an `Authorization` header and a fresh DPoP proof. The proof must be signed by the **same** RSA key that was active during login (the access token's `cnf.jkt` claim is bound to it):

```dart
Future<http.Response> getPrivateResource(
SolidAuthManager auth,
String resourceUrl,
) async {
final authData = auth.currentAuthData!;

final dpop = await DpopTokenGenerator.generateForRequest(
endpointUrl: resourceUrl,
httpMethod: 'GET',
accessToken: authData.accessToken,
keyManager: auth.keyManager,
);

return http.get(
Uri.parse(resourceUrl),
headers: {
'Authorization': 'DPoP ${authData.accessToken}',
'DPoP': dpop,
},
);
}
```

> **Important:** Always pass `keyManager: auth.keyManager`. The default (`DpopKeyManager.getInstance()`) returns the current singleton, which may be a freshly-generated key after an app restart, producing a thumbprint that the server will reject.

---

## Platform Setup

`redirectUri` and `postLogoutRedirectUri` must be registered in your [client ID document](https://solid.github.io/solid-oidc/#clientids-document) (`client-profile.jsonld`) 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 | `com.example.app://redirect` | Custom URI scheme registered with the OS |
| Windows / Linux / macOS | `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 your `redirectUri`. 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:

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

For Android, iOS, and other platform-specific setup steps (manifest entries, URL schemes, etc.), follow the [`package:oidc` Getting Started guide](https://bdaya-dev.github.io/oidc/oidc-getting-started/).

---

## Migration Guide - 0.1.x → 0.2.x

Platform-specific setup (Android `build.gradle`, iOS `Info.plist`,
web `redirect.html`, etc.) follows `package:oidc` requirements exactly.
See the [oidc Getting Started guide](https://bdaya-dev.github.io/oidc/oidc-getting-started/).
> [!IMPORTANT]
> Upgrading from `0.1.x` to `0.2.x` is a **breaking change**. The underlying architecture has been re-engineered. The forked `openid_client` is replaced by the OpenID-certified `package:oidc`, and the top-level `authenticate()` function is replaced by `SolidAuthManager`. Please review the full README and use the table below to update your call sites.

The old `callback.html` for web should be replaced by the
[`redirect.html`](https://github.com/Bdaya-Dev/oidc/blob/main/packages/oidc/example/web/redirect.html)
from `package:oidc`.
| Old (0.1.x) | New (0.2.x) |
|---|---|
| `String issuer = await getIssuer(webId)` | `WebIdUtils.getIssuer(webId)` (same signature) |
| `var data = await authenticate(issuerUri, scopes)` | `await SolidAuthManager.authenticate(webIdOrIssuer)` - returns `SolidAuthData` |
| `data['accessToken']` | `authData.accessToken` |
| `data['idToken']` | `authData.idToken` |
| `genDpopToken(url, keyPair, jwk, method)` | `DpopTokenGenerator.generateForRequest(endpointUrl:, httpMethod:, accessToken:, keyManager: auth.keyManager)` |
| `fetchProfileData(webId)` | Removed - use `http` + parse the Turtle response directly |
| *(new)* | `auth.tryRestoreSession()` - silent session restore on app startup |
2 changes: 1 addition & 1 deletion example/.metadata
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ migration:
- platform: root
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
- platform: windows
- platform: web
create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0

Expand Down
17 changes: 11 additions & 6 deletions example/lib/components/Header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,17 @@ class Header extends StatelessWidget {
color: Colors.black,
),
),
onPressed: () {
authManager.logout();
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => LoginScreen()),
);
onPressed: () async {
// Await logout so the browser completes the end-session
// redirect before we navigate away.
await authManager.logout();
if (context.mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => LoginScreen()),
);
}
},
)
: IconButton(
Expand Down
Loading
Loading