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;
}