From cffb01d2c586208f0924f0dbfa8f7ee192fb6015 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 15 May 2026 10:17:49 +0200 Subject: [PATCH 1/3] fix: route ASWebAuthenticationSession URLs Apps like Slack and Claude Desktop use `ASWebAuthenticationSession` for SSO/OAuth sign-in flows. Those requests do not go through the normal http/https default-browser `Launch Services` path; macOS only forwards them to the default browser if it declares web authentication session support in `Info.plist`. Without that declaration, the system falls back to Safari before Finicky can apply any routing rules. https://developer.apple.com/documentation/authenticationservices/supporting-single-sign-on-in-a-web-browser-app#Declare-the-Session-Handling-Capability This declares Finicky as an `ASWebAuthenticationSession`-capable browser and register a session handler that forwards incoming authentication URLs through the existing URL handling pipeline. However, it does not declare ephemeral browser session support (`EphemeralBrowserSessionIsSupported`). Finicky routes URLs to another browser rather than owning the browsing context itself, so it cannot reliably guarantee that cookies, storage, or profile state are isolated for requests where `AuthenticationServices` asks for an ephemeral session. At this time, advertising only the base capability should be the minimal and honest thing to do, if this becomes an issue this choice can be re-evaluated. --- apps/finicky/assets/Info.plist | 7 ++- apps/finicky/src/main.go | 2 +- apps/finicky/src/main.m | 81 ++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/apps/finicky/assets/Info.plist b/apps/finicky/assets/Info.plist index 64dd522..cb49015 100644 --- a/apps/finicky/assets/Info.plist +++ b/apps/finicky/assets/Info.plist @@ -22,6 +22,11 @@ 4.4.0-alpha CFBundleIconFile finicky.icns + ASWebAuthenticationSessionWebBrowserSupportCapabilities + + IsSupported + + LSUIElement CFBundleURLTypes @@ -77,4 +82,4 @@ NSUserActivityTypeBrowsingWeb - \ No newline at end of file + diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index 6297c9c..c8c463d 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -2,7 +2,7 @@ package main /* #cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Cocoa -framework CoreServices +#cgo LDFLAGS: -framework Cocoa -framework CoreServices -framework AuthenticationServices #include #include "main.h" */ diff --git a/apps/finicky/src/main.m b/apps/finicky/src/main.m index ad2aa53..8b70970 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -2,14 +2,75 @@ #include "util/info.h" #import #import +#import #import #import #import "window/window.h" // For ShowWindow() +@interface FinickyAuthenticationSessionHandler : NSObject +@property (nonatomic, strong) NSMutableDictionary *requests; +- (BOOL)completeRequestWithCallbackURL:(NSURL *)url; +@end + +@implementation FinickyAuthenticationSessionHandler + +- (instancetype)init { + self = [super init]; + if (self) { + _requests = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)beginHandlingWebAuthenticationSessionRequest:(ASWebAuthenticationSessionRequest *)request { + if (!request.URL) { + NSError *error = [NSError errorWithDomain:@"se.johnste.finicky.authentication" + code:1 + userInfo:@{NSLocalizedDescriptionKey: @"Authentication request did not include a URL"}]; + [request cancelWithError:error]; + return; + } + + self.requests[request.UUID] = request; + HandleURL((char *)[[request.URL absoluteString] UTF8String], NULL, NULL, NULL, NULL, false); +} + +- (void)cancelWebAuthenticationSessionRequest:(ASWebAuthenticationSessionRequest *)request { + [self.requests removeObjectForKey:request.UUID]; +} + +- (BOOL)completeRequestWithCallbackURL:(NSURL *)url { + if (!url.scheme) { + return NO; + } + + for (NSUUID *uuid in [self.requests allKeys]) { + ASWebAuthenticationSessionRequest *request = self.requests[uuid]; + NSString *callbackURLScheme = request.callbackURLScheme; + if (callbackURLScheme.length == 0) { + continue; + } + if ([callbackURLScheme caseInsensitiveCompare:@"http"] == NSOrderedSame || + [callbackURLScheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { + continue; + } + if ([url.scheme caseInsensitiveCompare:callbackURLScheme] == NSOrderedSame) { + [request completeWithCallbackURL:url]; + [self.requests removeObjectForKey:uuid]; + return YES; + } + } + + return NO; +} + +@end + // Extend BrowseAppDelegate to hold a status item and declare menu action @interface BrowseAppDelegate () @property (nonatomic, strong) NSStatusItem *statusItem; +@property (nonatomic, strong) FinickyAuthenticationSessionHandler *authenticationSessionHandler; - (void)showWindowAction:(id)sender; @end @@ -30,16 +91,17 @@ - (instancetype)initWithForceOpenWindow:(bool)forceOpenWindow initShow:(bool)sho - (void)applicationDidFinishLaunching:(NSNotification *)notification { [self terminateOtherInstances]; + bool launchedByAuthenticationServices = [ASWebAuthenticationSessionWebBrowserSessionManager sharedManager].wasLaunchedByAuthenticationServices; bool openWindow = self.forceOpenWindow; if (!openWindow) { // Even if we aren't forcing the window to open, we still want to open it if didn't receive a URL - openWindow = !self.receivedURL; + openWindow = !self.receivedURL && !launchedByAuthenticationServices; } // Only show menu item if the option is enabled, and we either didn't receive a URL or we are keeping // the application running. We don't want to show the icon if Finicky is just receiving a url to open // and is expected to exit after - if (self.showMenuItem && (self.keepRunning || !self.receivedURL)) { + if (self.showMenuItem && (self.keepRunning || (!self.receivedURL && !launchedByAuthenticationServices))) { [self createStatusItem]; } @@ -162,6 +224,9 @@ - (void)applicationWillFinishLaunching:(NSNotification *)aNotification [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; + + self.authenticationSessionHandler = [[FinickyAuthenticationSessionHandler alloc] init]; + [ASWebAuthenticationSessionWebBrowserSessionManager sharedManager].sessionHandler = self.authenticationSessionHandler; } - (bool)application:(NSApplication *)sender openFile:(NSString *)filename { @@ -185,13 +250,19 @@ - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event // Get the application that opened the URL, if available int32_t pid = [[event attributeDescriptorForKeyword:keySenderPIDAttr] int32Value]; NSRunningApplication *application = [NSRunningApplication runningApplicationWithProcessIdentifier:pid]; - const char *url = [[[event paramDescriptorForKeyword:keyDirectObject] stringValue] UTF8String]; + NSString *urlString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + const char *url = [urlString UTF8String]; const char *name = NULL; const char *bundleId = NULL; const char *path = NULL; self.receivedURL = true; + NSURL *eventURL = [NSURL URLWithString:urlString]; + if ([self.authenticationSessionHandler completeRequestWithCallbackURL:eventURL]) { + return; + } + NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; // Assume Finicky is in front if we are not keeping running, since there's no good way @@ -249,6 +320,10 @@ - (bool)application:(NSApplication *)application continueUserActivity:(NSUserAct return false; } + if ([self.authenticationSessionHandler completeRequestWithCallbackURL:url]) { + return true; + } + HandleURL((char*)[[url absoluteString] UTF8String], NULL, NULL, NULL, NULL, false); return true; } From 6d1d1885678122798afbdffa99b5b62424a9e5a9 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 15 May 2026 15:05:33 +0200 Subject: [PATCH 2/3] fix: install auth session handler during startup Install the ASWebAuthenticationSession handler before Finicky finishes its normal config bootstrap, so launches initiated by AuthenticationServices have a handler available immediately. Forward authentication requests through the normal URL resolver with a synthetic AuthenticationServices opener, while preserving best-effort callback completion for callback URLs that come back through Finicky. Keep the http/https URL type separate from the finicky scheme so Launch Services sees the browser URL claim cleanly. --- apps/finicky/assets/Info.plist | 9 +++++++++ apps/finicky/src/main.go | 1 + apps/finicky/src/main.h | 1 + apps/finicky/src/main.m | 33 +++++++++++++++++++++++++++------ 4 files changed, 38 insertions(+), 6 deletions(-) diff --git a/apps/finicky/assets/Info.plist b/apps/finicky/assets/Info.plist index cb49015..673b118 100644 --- a/apps/finicky/assets/Info.plist +++ b/apps/finicky/assets/Info.plist @@ -40,6 +40,15 @@ http https + + + + CFBundleTypeRole + Viewer + CFBundleURLName + Finicky URL + CFBundleURLSchemes + finicky diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index c8c463d..944ee0b 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -68,6 +68,7 @@ func main() { startTime := time.Now() logger.Setup() runtime.LockOSThread() + C.InstallAuthenticationSessionHandler() // Define command line flags configPathPtr := flag.String("config", "", "Path to custom JS configuration file") diff --git a/apps/finicky/src/main.h b/apps/finicky/src/main.h index e8029c4..2c5992a 100644 --- a/apps/finicky/src/main.h +++ b/apps/finicky/src/main.h @@ -27,5 +27,6 @@ extern char* GetCurrentConfigPath(); #endif void RunApp(bool forceOpenWindow, bool showStatusItem, bool keepRunning); +void InstallAuthenticationSessionHandler(void); #endif /* MAIN_H */ diff --git a/apps/finicky/src/main.m b/apps/finicky/src/main.m index 8b70970..a91b145 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -33,7 +33,17 @@ - (void)beginHandlingWebAuthenticationSessionRequest:(ASWebAuthenticationSession } self.requests[request.UUID] = request; - HandleURL((char *)[[request.URL absoluteString] UTF8String], NULL, NULL, NULL, NULL, false); + + // AuthenticationServices does not expose the requesting app here, and + // Finicky cannot transfer the ASWebAuthenticationSessionRequest to the + // browser it chooses. Treat this as a normal URL routing request with a + // stable synthetic opener so rules can target SSO/OAuth flows explicitly. + HandleURL((char *)[[request.URL absoluteString] UTF8String], + "AuthenticationServices", + "com.apple.AuthenticationServices", + "", + NULL, + false); } - (void)cancelWebAuthenticationSessionRequest:(ASWebAuthenticationSessionRequest *)request { @@ -47,7 +57,10 @@ - (BOOL)completeRequestWithCallbackURL:(NSURL *)url { for (NSUUID *uuid in [self.requests allKeys]) { ASWebAuthenticationSessionRequest *request = self.requests[uuid]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" NSString *callbackURLScheme = request.callbackURLScheme; +#pragma clang diagnostic pop if (callbackURLScheme.length == 0) { continue; } @@ -67,10 +80,19 @@ - (BOOL)completeRequestWithCallbackURL:(NSURL *)url { @end +static FinickyAuthenticationSessionHandler *sharedAuthenticationSessionHandler; + +void InstallAuthenticationSessionHandler(void) { + if (!sharedAuthenticationSessionHandler) { + sharedAuthenticationSessionHandler = [[FinickyAuthenticationSessionHandler alloc] init]; + } + + [ASWebAuthenticationSessionWebBrowserSessionManager sharedManager].sessionHandler = sharedAuthenticationSessionHandler; +} + // Extend BrowseAppDelegate to hold a status item and declare menu action @interface BrowseAppDelegate () @property (nonatomic, strong) NSStatusItem *statusItem; -@property (nonatomic, strong) FinickyAuthenticationSessionHandler *authenticationSessionHandler; - (void)showWindowAction:(id)sender; @end @@ -225,8 +247,7 @@ - (void)applicationWillFinishLaunching:(NSNotification *)aNotification andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; - self.authenticationSessionHandler = [[FinickyAuthenticationSessionHandler alloc] init]; - [ASWebAuthenticationSessionWebBrowserSessionManager sharedManager].sessionHandler = self.authenticationSessionHandler; + InstallAuthenticationSessionHandler(); } - (bool)application:(NSApplication *)sender openFile:(NSString *)filename { @@ -259,7 +280,7 @@ - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event self.receivedURL = true; NSURL *eventURL = [NSURL URLWithString:urlString]; - if ([self.authenticationSessionHandler completeRequestWithCallbackURL:eventURL]) { + if ([sharedAuthenticationSessionHandler completeRequestWithCallbackURL:eventURL]) { return; } @@ -320,7 +341,7 @@ - (bool)application:(NSApplication *)application continueUserActivity:(NSUserAct return false; } - if ([self.authenticationSessionHandler completeRequestWithCallbackURL:url]) { + if ([sharedAuthenticationSessionHandler completeRequestWithCallbackURL:url]) { return true; } From de1f9e8d12f692e4c7fe82118ceae543078e9883 Mon Sep 17 00:00:00 2001 From: Brice Dutheil Date: Fri, 15 May 2026 15:42:47 +0200 Subject: [PATCH 3/3] fix: declare best-effort auth callback matching Some ASWebAuthenticationSession clients require the selected browser to advertise callback URL matching before they will route SSO requests through it. Finicky can complete callbacks that re-enter Finicky, which preserves custom-scheme flows and gives callback-aware clients a route through the existing browser rules. This is intentionally best-effort: once Finicky forwards the initial auth URL to the concrete browser selected by user rules, it cannot observe subsequent navigations in that browser. Document that limitation next to the callback completion code. Do not declare ephemeral session support yet, because Finicky does not currently force an incognito or private session in the selected browser and should not claim stronger privacy semantics than it can provide. --- apps/finicky/assets/Info.plist | 2 ++ apps/finicky/src/main.m | 3 +++ 2 files changed, 5 insertions(+) diff --git a/apps/finicky/assets/Info.plist b/apps/finicky/assets/Info.plist index 673b118..49d6ed4 100644 --- a/apps/finicky/assets/Info.plist +++ b/apps/finicky/assets/Info.plist @@ -26,6 +26,8 @@ IsSupported + CallbackURLMatchingIsSupported + LSUIElement diff --git a/apps/finicky/src/main.m b/apps/finicky/src/main.m index a91b145..cabb0ca 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -55,6 +55,9 @@ - (BOOL)completeRequestWithCallbackURL:(NSURL *)url { return NO; } + // Callback matching is best-effort: Finicky can complete callbacks that + // re-enter Finicky, but it cannot observe navigations after forwarding the + // auth URL to the concrete browser selected by the user's rules. for (NSUUID *uuid in [self.requests allKeys]) { ASWebAuthenticationSessionRequest *request = self.requests[uuid]; #pragma clang diagnostic push