Skip to content
Merged
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
69 changes: 67 additions & 2 deletions lib/solid_auth_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,47 @@ PlatformInfo currPlatform = PlatformInfo();

AuthManager authManager = AuthManager();

/// Thrown by [authenticate] when the in-flight OAuth flow is aborted via
/// [cancelAuthenticate]. Lets callers distinguish a deliberate user (or
/// programmatic) cancellation from a genuine network/server failure so they
/// can suppress error UI accordingly.

class AuthCancelledException implements Exception {
final String message;

const AuthCancelledException([
this.message = 'Authentication was cancelled',
]);

@override
String toString() => 'AuthCancelledException: $message';
}

/// Reference to the desktop [oidc_mobile.Authenticator] currently waiting on
/// the OAuth callback. Tracked here so [cancelAuthenticate] can tear it down
/// from outside the awaiting code path without having to thread the handle
/// through the call site.

oidc_mobile.Authenticator? _pendingAuthenticator;

/// Returns true while a desktop [authenticate] call is awaiting the OAuth
/// browser redirect. Useful for UI code that wants to gate retry buttons
/// while a previous attempt is still pending.

bool isAuthenticatePending() => _pendingAuthenticator != null;

/// Aborts any in-flight desktop [authenticate] call. The awaited future
/// throws an [AuthCancelledException], the local OAuth callback HTTP server
/// is closed when no other flow needs it, and the cached authenticator
/// reference is cleared.

Future<void> cancelAuthenticate() async {
final pending = _pendingAuthenticator;
if (pending == null) return;
_pendingAuthenticator = null;
await pending.cancel();
}

/// Dynamically register the user in the POD server
Future<String> clientDynamicReg(
String regEndpoint,
Expand Down Expand Up @@ -260,8 +301,32 @@ Future<Map> authenticate(
'Authentication process completed. You can now close this window!',
);

/// starts the authentication + authorisation process
authResponse = await authenticator.authorize();
// Publish the in-flight authenticator so [cancelAuthenticate] can tear
// it down from outside this call. Any previously pending authenticator
// is cancelled first to free the shared local OAuth callback server.
final previous = _pendingAuthenticator;
if (previous != null) {
try {
await previous.cancel();
} on Object {
// Best-effort cleanup; ignore failures from the prior flow.
}
}
_pendingAuthenticator = authenticator;

try {
/// starts the authentication + authorisation process
authResponse = await authenticator.authorize();
} on Exception {
if (!identical(_pendingAuthenticator, authenticator)) {
throw const AuthCancelledException();
}
rethrow;
} finally {
if (identical(_pendingAuthenticator, authenticator)) {
_pendingAuthenticator = null;
}
}

/// close the webview when finished
/// closing web view function does not work in Windows applications
Expand Down
35 changes: 28 additions & 7 deletions lib/src/openid/openid_client_io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,37 @@ class Authenticator {

/// Cancels the authentication flow.
///
/// This method will stop the local http server and complete the [authorize]
/// method with an error.
/// Errors the [authorize] future for this authenticator's state with a
/// `Flow was cancelled` [Exception] and tears down the shared local http
/// server when there are no other in-flight authentications waiting on it.
///
/// This method should be called when the user cancels the authentication flow
/// in the browser.
/// Idempotent: safe to call multiple times, and a no-op when the flow has
/// already completed or has no registered state. This matters on desktop
/// where there is no reliable signal that the user has closed the external
/// browser window, so cancellation may be requested speculatively.
Future<void> cancel() async {
final state = flow.authenticationUri.queryParameters['state'];
_requestsByState[state!]?.completeError(Exception('Flow was cancelled'));
final server = await _requestServers.remove(port);
await server?.close();
if (state == null) return;

final completer = _requestsByState.remove(state);
if (completer != null && !completer.isCompleted) {
completer.completeError(Exception('Flow was cancelled'));
Comment on lines +149 to +151
}

// Only stop the local OAuth callback server when no other states are
// still waiting for a redirect. Closing it whilst another authenticator
// is still in flight would strand that flow on a dead socket.
if (_requestsByState.isEmpty) {
final serverFuture = _requestServers.remove(port);
Comment on lines +157 to +158
if (serverFuture != null) {
try {
final server = await serverFuture;
await server.close(force: true);
} on Object {
// Server may already be closed or never finished binding — ignore.
}
}
}
}

static final Map<int, Future<HttpServer>> _requestServers = {};
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ dependencies:
dev_dependencies:
flutter_lints: ^5.0.0
jwt_decoder: ^2.0.1
# Keep dependency checker quiet.
# Keep the dependency checker quiet.
solid_auth_example:
path: example

Expand Down
Loading