diff --git a/apps/finicky/assets/Info.plist b/apps/finicky/assets/Info.plist index 64dd522..49d6ed4 100644 --- a/apps/finicky/assets/Info.plist +++ b/apps/finicky/assets/Info.plist @@ -22,6 +22,13 @@ 4.4.0-alpha CFBundleIconFile finicky.icns + ASWebAuthenticationSessionWebBrowserSupportCapabilities + + IsSupported + + CallbackURLMatchingIsSupported + + LSUIElement CFBundleURLTypes @@ -35,6 +42,15 @@ http https + + + + CFBundleTypeRole + Viewer + CFBundleURLName + Finicky URL + CFBundleURLSchemes + finicky @@ -77,4 +93,4 @@ NSUserActivityTypeBrowsingWeb - \ No newline at end of file + diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index 6297c9c..944ee0b 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" */ @@ -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 ad2aa53..cabb0ca 100644 --- a/apps/finicky/src/main.m +++ b/apps/finicky/src/main.m @@ -2,11 +2,97 @@ #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; + + // 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 { + [self.requests removeObjectForKey:request.UUID]; +} + +- (BOOL)completeRequestWithCallbackURL:(NSURL *)url { + if (!url.scheme) { + 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 +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + NSString *callbackURLScheme = request.callbackURLScheme; +#pragma clang diagnostic pop + 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 + +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; @@ -30,16 +116,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 +249,8 @@ - (void)applicationWillFinishLaunching:(NSNotification *)aNotification [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; + + InstallAuthenticationSessionHandler(); } - (bool)application:(NSApplication *)sender openFile:(NSString *)filename { @@ -185,13 +274,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 ([sharedAuthenticationSessionHandler 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 +344,10 @@ - (bool)application:(NSApplication *)application continueUserActivity:(NSUserAct return false; } + if ([sharedAuthenticationSessionHandler completeRequestWithCallbackURL:url]) { + return true; + } + HandleURL((char*)[[url absoluteString] UTF8String], NULL, NULL, NULL, NULL, false); return true; }