AppAuth-iOS icon indicating copy to clipboard operation
AppAuth-iOS copied to clipboard

application:openURL:options: not called

Open long1eu opened this issue 5 years ago • 11 comments

I'm using this in Flutter where I have just the rootViewController. It all works well I can login, the server redirects to the custom scheme of the app.

In my case I redirect the code to the my backend server and from there to the app. After the server responds the app pops up but the callback is not called, nether the application:openURL:options:

What am I missing.

#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"

static NSString *const kAppAuthExampleAuthStateKey = @"authState";

@implementation AppDelegate {
    OIDServiceConfiguration *configuration;
    OIDAuthState *authState;
    FlutterResult flutterResult;
}

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  
    FlutterViewController *controller = (FlutterViewController *) [[self window] rootViewController];
    FlutterMethodChannel *channel =
        [FlutterMethodChannel methodChannelWithName:@"app_channel" binaryMessenger: controller];

    [channel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult  _Nonnull result) {
        [self onMethodCall:call result:result];
    }];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

    
- (void) onMethodCall:(FlutterMethodCall *)call result:(FlutterResult )result {
    if([@"init" isEqualToString:call.method]){
      OIDServiceDiscovery *doc = [[OIDServiceDiscovery alloc] initWithJSON:call.arguments error:nil];
      configuration = [[OIDServiceConfiguration alloc] initWithDiscoveryDocument:doc];
    } else if([@"authorize" isEqualToString:call.method]) {
        if (flutterResult) {
            flutterResult = nil;
            @throw [NSInvalidArgumentException initWithString:@"Already login in."];
        }
        flutterResult = result;
        NSString *jsonString = call.arguments[@"params"];
        NSStringEncoding encoding = 0;
        NSData *jsonData = [jsonString dataUsingEncoding:encoding];
        NSError *error=nil;
        NSDictionary *params = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:&error];
        
        NSMutableDictionary *additionalParameters = [[NSMutableDictionary alloc] initWithDictionary: params[@"additionalParameters"]];
        
        if (params[@"display"]) {
            [additionalParameters setValue:params[@"display"] forKey:@"display"];
        }
        
        if (params[@"prompt"]) {
            [additionalParameters setValue:params[@"prompt"] forKey:@"prompt"];
        }
        NSString *nonce = additionalParameters[@"nonce"];
        [additionalParameters removeObjectForKey: @"nonce"];
        
        OIDAuthorizationRequest *request =
        [[OIDAuthorizationRequest alloc]
         initWithConfiguration: configuration
         clientId: params[@"clientId"]
         clientSecret:nil
         scope: params[@"scope"]
         redirectURL: [NSURL URLWithString:params[@"redirectUri"]]
         responseType: params[@"responseType"]
         state: params[@"state"]
         nonce: nonce
         codeVerifier: nil
         codeChallenge: nil
         codeChallengeMethod: nil
         additionalParameters: additionalParameters];
        
        _currentAuthorizationFlow = [OIDAuthState
         authStateByPresentingAuthorizationRequest:request
         presentingViewController:[[self window] rootViewController]
         callback:^(OIDAuthState * _Nullable authState, NSError * _Nullable error) {
             NSLog(@"%@", [[authState lastAuthorizationResponse] additionalParameters]);
             if (authState) {
                 [self setAuthState:authState];
                 
             } else {
                 result([FlutterError errorWithCode:error.description message:error.description details:nil]);
                 [self setAuthState:nil];
             }
         }];
    } else {
        result(FlutterMethodNotImplemented);
    }
}
    
- (BOOL)application:(UIApplication *)app
            openURL:(NSURL *)url
            options:(NSDictionary<NSString *, id> *)options {
    
    NSLog(@"a| %@", [url absoluteString]);
    
    // Sends the URL to the current authorization flow (if any) which will
    // process it if it relates to an authorization response.
    if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) {
        _currentAuthorizationFlow = nil;
        return YES;
    }
    
    NSLog(@"%@", [url absoluteString]);
    
    // Your additional URL handling (if any) goes here.
    
    return NO;
}
    
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    NSLog(@"%@", [url absoluteString]);
    return [self application:application openURL:url options:@{}];
}
    
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
    NSLog(@"%@", [url absoluteString]);
    return [self application:application openURL:url options:@{}];
}
    
- (void)didChangeState:(OIDAuthState *)state {
    [self saveState];
}
    
- (void)saveState {
    // for production usage consider using the OS Keychain instead
    NSData *archivedAuthState = [ NSKeyedArchiver archivedDataWithRootObject:_authState];
    [[NSUserDefaults standardUserDefaults] setObject:archivedAuthState
                                              forKey:kAppAuthExampleAuthStateKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}
    
- (void)loadState {
    // loads OIDAuthState from NSUSerDefaults
    NSData *archivedAuthState =
    [[NSUserDefaults standardUserDefaults] objectForKey:kAppAuthExampleAuthStateKey];
    OIDAuthState *authState = [NSKeyedUnarchiver unarchiveObjectWithData:archivedAuthState];
    [self setAuthState:authState];
}
    
- (void)setAuthState:(nullable OIDAuthState *)authState {
    if (_authState == authState) {
        return;
    }
    _authState = authState;
    _authState.stateChangeDelegate = self;
    [self saveState];
}
    
@end

long1eu avatar Feb 26 '19 21:02 long1eu

I've also tried this

       _currentAuthorizationFlow = [OIDAuthorizationService presentAuthorizationRequest:request
           presentingViewController:[[self window] rootViewController]
           callback:^(OIDAuthorizationResponse *authorizationResponse, NSError *error) {
               NSLog(@"callback called");
               if (authorizationResponse) {
                   OIDAuthState *authState =
                    [[OIDAuthState alloc] initWithAuthorizationResponse:authorizationResponse];
                   [self setAuthState:authState];
               } else {
                   result([FlutterError errorWithCode:error.description message:error.description details:nil]);
                   [self setAuthState:nil];
               }                            
           }];

long1eu avatar Feb 26 '19 21:02 long1eu

If application:openURL:options: isn't being called, then my guess is you haven't registered the URI scheme in your Info plist.

WilliamDenniss avatar Mar 12 '19 16:03 WilliamDenniss

@long1eu What redirect URI protocol are you using? a custom scheme or a https universal link? If it's the latter, it could possibly be related issue #367

grEvenX avatar Mar 25 '19 20:03 grEvenX

I am seeing the same issue. If I set the redirect URL as my app's custom URL directly (e.g. "myapp://oauth-redirect"), then the callback is called. But if I set the redirect URL to a page on my server, then redirect to the same custom URL, the webview closes but the callback is not called (nor is my app delegate's application:openURL:options.

sjmerel avatar Mar 26 '19 17:03 sjmerel

I can confirm this behavior.

On Tue, Mar 26, 2019, 17:01 sjmerel [email protected] wrote:

I am seeing the same issue. If I set the redirect URL as my app's custom URL directly (e.g. "myapp://oauth-redirect"), then the callback is called. But if I set the redirect URL to a page on my server, then redirect to the same custom URL, the webview closes but the callback is not called (nor is my app delegate's application:openURL:options.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/openid/AppAuth-iOS/issues/356#issuecomment-476747976, or mute the thread https://github.com/notifications/unsubscribe-auth/AI2nVqD93y2fw4dYSNk8gyJoC9J2QSyOks5valJvgaJpZM4bTKLv .

long1eu avatar Mar 26 '19 17:03 long1eu

The problem seems to be here, in OIDAuthorizationService.m:

- (BOOL)shouldHandleURL:(NSURL *)URL {
  NSURL *standardizedURL = [URL standardizedURL];
  NSURL *standardizedRedirectURL = [_request.redirectURL standardizedURL];

  return OIDIsEqualIncludingNil(standardizedURL.scheme, standardizedRedirectURL.scheme) &&
      OIDIsEqualIncludingNil(standardizedURL.user, standardizedRedirectURL.user) &&
      OIDIsEqualIncludingNil(standardizedURL.password, standardizedRedirectURL.password) &&
      OIDIsEqualIncludingNil(standardizedURL.host, standardizedRedirectURL.host) &&
      OIDIsEqualIncludingNil(standardizedURL.port, standardizedRedirectURL.port) &&
      OIDIsEqualIncludingNil(standardizedURL.path, standardizedRedirectURL.path);
}

standardizedURL is the redirect URL that I passed in to my initial request (that matches the one I've set with my identity provider, Fitbit in my case). standardizedRedirectURL is where that page is sending the user (my app's custom URL).

Is this check due to a security concern?

AppAuth on Android doesn't have this problem. (The Android version of my app is the reason why I'm redirecting to a web page in the first place; redirecting straight to the app from the IdP's signon page is somewhat confusing on Android.)

sjmerel avatar Mar 26 '19 18:03 sjmerel

@sjmerel The problem is probably not in this code in that case. The code you mentioned is only called as long as you are properly forwarding the URL to the resumeExternalUserAgentFlowWithURL method.

In order to catch a Universal Link you need to implement that code in the correct UIApplicationDelegate method and it only works if you have taken proper measures to use a Universal Link. The blog post https://medium.com/@abhimuralidharan/universal-links-in-ios-79c4ee038272 explains it pretty well.

If you have implemented Universal Link properly, you should be able to paste the callback link in Messages app, or link to it from a website or similar, and when you tap on it, the app should open and handle the link.

grEvenX avatar Mar 26 '19 19:03 grEvenX

@grEvenX Not sure what you mean... shouldHandleURL is getting called; it's returning false, though, which causes resumeExternalUserAgentFlowWithURL to exit early.

I'm using a Custom URL Scheme, not an Universal Link (though I could use Universal Links instead). I know my scheme is set up correctly because if I enter it into Safari directly, I am prompted to open my app.

sjmerel avatar Mar 26 '19 19:03 sjmerel

I have the same Problem. I have a successful login but the redirect URL doesn't trigger the appDelegate. I have pass my redirectURL( appName://authenticate )

I have configure in the info.plist all the needed info correct.

Nothing get triggered .

Any idea why ?

Here is my code

class func signInAuth(discoveryURLstr: String,presenter : UIViewController,completionHandler: @escaping ( (OIDAuthState?,Error?) -> () )){
        
        guard let discoveruURL = URL(string: discoveryURLstr) else{
            completionHandler(nil,AuthErrors.InvalidDiscoveryURL)
            return
        }
        
        appAuthDiscoverConfiguration(discoveryURL: discoveruURL) { (configurationFile, error) in
            
            guard let configurationFile = configurationFile else {
                completionHandler(nil,AuthErrors.InvalidConfigurationFile)
                return
            }
            
            let authRequest = appAuthRequest(configurationFile: configurationFile)
            
             self.appAuthenticationSession = OIDAuthState.authState(byPresenting: authRequest, presenting: presenter, callback: { (state, error) in

                if let error = error {
                    //self.authState = nil
                    completionHandler(nil,error)
                    return
                }

                if let state = state {
                    self.authState = state
                    completionHandler(state,nil)

                }else{
                    completionHandler(nil,AuthErrors.InvalideState)
                }
            })
            
        }
        
        
    }
    
    class func appAuthDiscoverConfiguration(discoveryURL : URL, completionHandler: @escaping ((OIDServiceConfiguration?,Error?) -> ())) {

        OIDAuthorizationService.discoverConfiguration(forDiscoveryURL: discoveryURL) { (configuration, error) in
            
            if let error = error {
                completionHandler(nil,error)
                return
            }else{
                guard let configurationFile = configuration else {
                    completionHandler(nil,AuthErrors.InvalidConfigurationFile)
                    return
                }
                completionHandler(configurationFile,nil)
            }
            
        }
        
    }
    
    class func appAuthRequest(configurationFile : OIDServiceConfiguration) -> OIDAuthorizationRequest{
        
  
        return OIDAuthorizationRequest(configuration: configurationFile, clientId: AppAuthConstants.clientId, clientSecret: nil, scope: AppAuthConstants.scope, redirectURL: AppAuthConstants.redirectURL, responseType: AppAuthConstants.responseType, state: nil, nonce: nil, codeVerifier: nil, codeChallenge: nil, codeChallengeMethod: nil, additionalParameters: AppAuthConstants.additionalParameters)
    }

grNorway avatar May 28 '19 15:05 grNorway

Looks like this issue has been discussed at length in #232

lapinek avatar May 29 '19 23:05 lapinek

I am seeing the same issue. If I set the redirect URL as my app's custom URL directly (e.g. "myapp://oauth-redirect"), then the callback is called. But if I set the redirect URL to a page on my server, then redirect to the same custom URL, the webview closes but the callback is not called (nor is my app delegate's application:openURL:options.

thanks, Your suggestion solved my problem in the demo where i download from https://github.com/insanelydeepak/fitbit-api-example-iOS which login to fitbit by using SFSafariViewController

zj381652512 avatar Sep 20 '22 03:09 zj381652512