From 33efc784960525dede3173acee3ac0bd8014361b Mon Sep 17 00:00:00 2001 From: Cheng Date: Tue, 17 Sep 2024 13:42:07 -0400 Subject: [PATCH 1/2] app_auth platform interface: implement SFSafariController --- .../flutter_appauth_platform_interface.dart | 1 + .../lib/src/authorization_parameters.dart | 14 ++++++++++---- .../lib/src/authorization_request.dart | 6 ++++-- .../lib/src/authorization_token_request.dart | 6 ++++-- .../lib/src/end_session_request.dart | 19 +++++++++++-------- .../lib/src/external_agent_type.dart | 17 +++++++++++++++++ .../lib/src/method_channel_mappers.dart | 5 +++-- .../method_channel_flutter_appauth_test.dart | 9 ++++++--- 8 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 flutter_appauth_platform_interface/lib/src/external_agent_type.dart diff --git a/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart b/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart index 16582ebe..c738b9cc 100644 --- a/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart +++ b/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart @@ -6,6 +6,7 @@ export 'src/authorization_token_response.dart'; export 'src/end_session_request.dart'; export 'src/end_session_response.dart'; export 'src/errors.dart'; +export 'src/external_agent_type.dart'; export 'src/flutter_appauth_platform.dart'; export 'src/grant_types.dart'; export 'src/token_request.dart'; diff --git a/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart b/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart index 94c427b4..ab74de96 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart @@ -1,3 +1,5 @@ +import 'external_agent_type.dart'; + mixin AuthorizationParameters { /// Hint to the Authorization Server about the login identifier the End-User /// might use to log in. @@ -7,11 +9,15 @@ mixin AuthorizationParameters { /// Server prompts the End-User for reauthentication and consent. List? promptValues; - /// Whether to use an ephemeral session that prevents cookies and other - /// browser data being shared with the user's normal browser session. - /// + /// Decides what type of external agent to use for the authorization flow. + /// ASWebAuthenticationSession is the default for iOS 12 and above. + /// EphemeralSession is not sharing browser data + /// with the user's normal browser session but not keeping the cache + /// SFSafariViewController is not sharing browser data + /// with the user's normal browser session but keeping the cache. /// This property is only applicable to iOS versions 13 and above. - bool? preferEphemeralSession; + /// ExternalAgentType? preferredExternalAgent; + ExternalAgentType? preferredExternalAgent; String? responseMode; } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_request.dart index b7b56fb1..8907c2a1 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_request.dart @@ -1,6 +1,7 @@ import 'authorization_parameters.dart'; import 'authorization_service_configuration.dart'; import 'common_request_details.dart'; +import 'external_agent_type.dart'; /// The details of an authorization request to get an authorization code. class AuthorizationRequest extends CommonRequestDetails @@ -16,7 +17,8 @@ class AuthorizationRequest extends CommonRequestDetails Map? additionalParameters, List? promptValues, bool allowInsecureConnections = false, - bool preferEphemeralSession = false, + ExternalAgentType preferredExternalAgent = + ExternalAgentType.asWebAuthenticationSession, String? nonce, String? responseMode, }) { @@ -30,7 +32,7 @@ class AuthorizationRequest extends CommonRequestDetails this.loginHint = loginHint; this.promptValues = promptValues; this.allowInsecureConnections = allowInsecureConnections; - this.preferEphemeralSession = preferEphemeralSession; + this.preferredExternalAgent = preferredExternalAgent; this.nonce = nonce; this.responseMode = responseMode; assertConfigurationInfo(); diff --git a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart index dad96b52..93ef5da5 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart @@ -1,4 +1,5 @@ import 'authorization_parameters.dart'; +import 'external_agent_type.dart'; import 'grant_types.dart'; import 'token_request.dart'; @@ -17,7 +18,8 @@ class AuthorizationTokenRequest extends TokenRequest super.discoveryUrl, List? promptValues, super.allowInsecureConnections, - bool preferEphemeralSession = false, + ExternalAgentType preferredExternalAgent = + ExternalAgentType.asWebAuthenticationSession, super.nonce, String? responseMode, }) : super( @@ -25,7 +27,7 @@ class AuthorizationTokenRequest extends TokenRequest ) { this.loginHint = loginHint; this.promptValues = promptValues; - this.preferEphemeralSession = preferEphemeralSession; + this.preferredExternalAgent = preferredExternalAgent; this.responseMode = responseMode; } } diff --git a/flutter_appauth_platform_interface/lib/src/end_session_request.dart b/flutter_appauth_platform_interface/lib/src/end_session_request.dart index 4f5f1d32..f1ba8e47 100644 --- a/flutter_appauth_platform_interface/lib/src/end_session_request.dart +++ b/flutter_appauth_platform_interface/lib/src/end_session_request.dart @@ -8,7 +8,7 @@ class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { this.postLogoutRedirectUrl, this.state, this.allowInsecureConnections = false, - this.preferEphemeralSession = false, + this.preferredExternalAgent = ExternalAgentType.asWebAuthenticationSession, this.additionalParameters, String? issuer, String? discoveryUrl, @@ -38,14 +38,17 @@ class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { /// This property is only applicable to Android. bool allowInsecureConnections; - /// Whether to use an ephemeral session that prevents cookies and other - /// browser data being shared with the user's normal browser session. + /// Decides what type of external agent to use for the authorization flow. + /// ASWebAuthenticationSession is the default for iOS 12 and above. + /// EphemeralSession is not sharing browser data + /// with the user's normal browser session but not keeping the cache + /// SFSafariViewController is not sharing browser data + /// with the user's normal browser session but keeping the cache. + /// This property is only applicable to iOS versions 13 and above. + /// ExternalAgentType? preferredExternalAgent; /// - /// This property is only applicable to iOS (versions 13 and above) and macOS. - /// - /// preferEphemeralSession = true must only be used here, if it is also used - /// for the sign in call. - bool preferEphemeralSession; + /// Sign in and out must have the same type. + ExternalAgentType? preferredExternalAgent; final Map? additionalParameters; } diff --git a/flutter_appauth_platform_interface/lib/src/external_agent_type.dart b/flutter_appauth_platform_interface/lib/src/external_agent_type.dart new file mode 100644 index 00000000..dd0f03e8 --- /dev/null +++ b/flutter_appauth_platform_interface/lib/src/external_agent_type.dart @@ -0,0 +1,17 @@ +// Enum representing the type of external agent +// to use for the authorization flow. +enum ExternalAgentType { + /// Uses ASWebAuthenticationSession, the default for iOS 12 and above. + asWebAuthenticationSession, + + /// Uses an ephemeral session that does not share browser data with + /// the user's normal browser session and does not keep the cache. + ephemeralAsWebAuthenticationSession, + + /// Uses SFSafariViewController, which does not share browser data + /// with the user's normal browser session but keeps the cache. + /// + /// This is only applicable to iOS, on macOS it will use the same behavior as + /// ASWebAuthenticationSession. + sfSafariViewController +} diff --git a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart index 95c64e00..77c24fd1 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart @@ -33,7 +33,7 @@ extension EndSessionRequestMapper on EndSessionRequest { 'issuer': issuer, 'discoveryUrl': discoveryUrl, 'serviceConfiguration': serviceConfiguration?.toMap(), - 'preferEphemeralSession': preferEphemeralSession, + 'preferredExternalAgent': preferredExternalAgent.toString(), }; } } @@ -99,7 +99,8 @@ Map _convertAuthorizationParametersToMap( return { 'loginHint': authorizationParameters.loginHint, 'promptValues': authorizationParameters.promptValues, - 'preferEphemeralSession': authorizationParameters.preferEphemeralSession, + 'preferredExternalAgent': + authorizationParameters.preferredExternalAgent.toString(), 'responseMode': authorizationParameters.responseMode, }; } diff --git a/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart b/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart index 3d4ec46f..c514d60f 100644 --- a/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart +++ b/flutter_appauth_platform_interface/test/method_channel_flutter_appauth_test.dart @@ -38,7 +38,8 @@ void main() { 'serviceConfiguration': null, 'additionalParameters': null, 'allowInsecureConnections': false, - 'preferEphemeralSession': false, + 'preferredExternalAgent': + ExternalAgentType.asWebAuthenticationSession.toString(), 'promptValues': null, 'responseMode': null, 'nonce': null, @@ -66,7 +67,8 @@ void main() { 'serviceConfiguration': null, 'additionalParameters': null, 'allowInsecureConnections': false, - 'preferEphemeralSession': false, + 'preferredExternalAgent': + ExternalAgentType.asWebAuthenticationSession.toString(), 'promptValues': null, 'clientSecret': null, 'refreshToken': null, @@ -184,7 +186,8 @@ void main() { 'issuer': null, 'discoveryUrl': 'someDiscoveryUrl', 'serviceConfiguration': null, - 'preferEphemeralSession': false, + 'preferredExternalAgent': + ExternalAgentType.asWebAuthenticationSession.toString(), }) ]); }); From 17030940e8e477b3b1bcac84c980be005307f016 Mon Sep 17 00:00:00 2001 From: Cheng Date: Thu, 26 Sep 2024 09:53:16 -0400 Subject: [PATCH 2/2] appauth: support SFSafariController --- flutter_appauth/example/lib/main.dart | 67 +++++-- .../ios/Classes/AppAuthIOSAuthorization.h | 1 + .../ios/Classes/AppAuthIOSAuthorization.m | 20 +- flutter_appauth/ios/Classes/FlutterAppAuth.h | 4 +- flutter_appauth/ios/Classes/FlutterAppAuth.m | 2 +- .../ios/Classes/FlutterAppauthPlugin.m | 12 +- ...ExternalUserAgentIOSSafariViewController.h | 68 +++++++ ...ExternalUserAgentIOSSafariViewController.m | 173 ++++++++++++++++++ flutter_appauth/lib/flutter_appauth.dart | 1 + .../macos/Classes/AppAuthMacOSAuthorization.m | 12 +- 10 files changed, 326 insertions(+), 34 deletions(-) create mode 100644 flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.h create mode 100644 flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.m diff --git a/flutter_appauth/example/lib/main.dart b/flutter_appauth/example/lib/main.dart index f9765dc7..bc80a3bf 100644 --- a/flutter_appauth/example/lib/main.dart +++ b/flutter_appauth/example/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io' show Platform; import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; @@ -107,7 +108,22 @@ class _MyAppState extends State { textAlign: TextAlign.center, ), onPressed: () => _signInWithAutoCodeExchange( - preferEphemeralSession: true), + preferredExternalAgent: ExternalAgentType + .ephemeralAsWebAuthenticationSession), + ), + ), + if (Platform.isIOS) + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + child: const Text( + 'Sign in with auto code exchange using ' + 'SFSafariViewController', + textAlign: TextAlign.center, + ), + onPressed: () => _signInWithAutoCodeExchange( + preferredExternalAgent: + ExternalAgentType.sfSafariViewController), ), ), ElevatedButton( @@ -123,6 +139,34 @@ class _MyAppState extends State { : null, child: const Text('End session'), ), + if (Platform.isIOS || Platform.isMacOS) + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: _idToken != null + ? () async { + await _endSession( + preferredExternalAgent: ExternalAgentType + .ephemeralAsWebAuthenticationSession); + } + : null, + child: + const Text('End session using ephemeral session'), + )), + if (Platform.isIOS) + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: _idToken != null + ? () async { + await _endSession( + preferredExternalAgent: ExternalAgentType + .sfSafariViewController); + } + : null, + child: const Text( + 'End session using SFSafariViewController'), + )), const SizedBox(height: 8), if (_error != null) Text(_error ?? ''), const SizedBox(height: 8), @@ -156,13 +200,16 @@ class _MyAppState extends State { ); } - Future _endSession() async { + Future _endSession( + {ExternalAgentType preferredExternalAgent = + ExternalAgentType.asWebAuthenticationSession}) async { try { _setBusyState(); await _appAuth.endSession(EndSessionRequest( idTokenHint: _idToken, postLogoutRedirectUrl: _postLogoutRedirectUrl, - serviceConfiguration: _serviceConfiguration)); + serviceConfiguration: _serviceConfiguration, + preferredExternalAgent: preferredExternalAgent)); _clearSessionInfo(); } catch (e) { _handleError(e); @@ -285,7 +332,8 @@ class _MyAppState extends State { } Future _signInWithAutoCodeExchange( - {bool preferEphemeralSession = false}) async { + {ExternalAgentType preferredExternalAgent = + ExternalAgentType.asWebAuthenticationSession}) async { try { _setBusyState(); @@ -295,13 +343,10 @@ class _MyAppState extends State { */ final AuthorizationTokenResponse result = await _appAuth.authorizeAndExchangeCode( - AuthorizationTokenRequest( - _clientId, - _redirectUrl, - serviceConfiguration: _serviceConfiguration, - scopes: _scopes, - preferEphemeralSession: preferEphemeralSession, - ), + AuthorizationTokenRequest(_clientId, _redirectUrl, + serviceConfiguration: _serviceConfiguration, + scopes: _scopes, + preferredExternalAgent: preferredExternalAgent), ); /* diff --git a/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.h b/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.h index d41a9579..9c80bc5d 100644 --- a/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.h +++ b/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.h @@ -1,6 +1,7 @@ #import #import #import "OIDExternalUserAgentIOSNoSSO.h" +#import "OIDExternalUserAgentIOSSafariViewController.h" #import "FlutterAppAuth.h" NS_ASSUME_NONNULL_BEGIN diff --git a/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.m b/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.m index 47d8b755..187b4a7f 100644 --- a/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.m +++ b/flutter_appauth/ios/Classes/AppAuthIOSAuthorization.m @@ -2,7 +2,7 @@ @implementation AppAuthIOSAuthorization -- (id) performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferEphemeralSession:(BOOL)preferEphemeralSession result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce{ +- (id) performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferredExternalAgent:(NSString*)preferredExternalAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce{ NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier]; NSString *codeChallenge = [OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier]; @@ -21,7 +21,7 @@ @implementation AppAuthIOSAuthorization additionalParameters:additionalParameters]; UIViewController *rootViewController = [self rootViewController]; if(exchangeCode) { - id externalUserAgent = [self userAgentWithViewController:rootViewController useEphemeralSession:preferEphemeralSession]; + id externalUserAgent = [self userAgentWithViewController:rootViewController preferredExternalAgent:preferredExternalAgent]; return [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:externalUserAgent callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { if(authState) { @@ -32,7 +32,7 @@ @implementation AppAuthIOSAuthorization } }]; } else { - id externalUserAgent = [self userAgentWithViewController:rootViewController useEphemeralSession:preferEphemeralSession]; + id externalUserAgent = [self userAgentWithViewController:rootViewController preferredExternalAgent:preferredExternalAgent]; return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:externalUserAgent callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) { if(authorizationResponse) { NSMutableDictionary *processedResponse = [[NSMutableDictionary alloc] init]; @@ -56,7 +56,7 @@ @implementation AppAuthIOSAuthorization additionalParameters:requestParameters.additionalParameters]; UIViewController *rootViewController = [self rootViewController]; - id externalUserAgent = [self userAgentWithViewController:rootViewController useEphemeralSession:requestParameters.preferEphemeralSession]; + id externalUserAgent = [self userAgentWithViewController:rootViewController preferredExternalAgent:requestParameters.preferredExternalAgent]; return [OIDAuthorizationService presentEndSessionRequest:endSessionRequest externalUserAgent:externalUserAgent callback:^(OIDEndSessionResponse * _Nullable endSessionResponse, NSError * _Nullable error) { @@ -71,13 +71,17 @@ @implementation AppAuthIOSAuthorization }]; } -- (id)userAgentWithViewController:(UIViewController *)rootViewController useEphemeralSession:(BOOL)useEphemeralSession { - if (useEphemeralSession) { +- (id)userAgentWithViewController:(UIViewController *)rootViewController preferredExternalAgent:(NSString*)preferredExternalAgent { + if ([preferredExternalAgent isEqual:@"ExternalAgentType.ephemeralAsWebAuthenticationSession"]) { return [[OIDExternalUserAgentIOSNoSSO alloc] initWithPresentingViewController:rootViewController]; + } else if ([preferredExternalAgent isEqual:@"ExternalAgentType.sfSafariViewController"]) { + return [[OIDExternalUserAgentIOSSafariViewController alloc] + initWithPresentingViewController:rootViewController]; + } else { + return [[OIDExternalUserAgentIOS alloc] + initWithPresentingViewController:rootViewController]; } - return [[OIDExternalUserAgentIOS alloc] - initWithPresentingViewController:rootViewController]; } - (UIViewController *)rootViewController { diff --git a/flutter_appauth/ios/Classes/FlutterAppAuth.h b/flutter_appauth/ios/Classes/FlutterAppAuth.h index 7191dda4..78715c1b 100644 --- a/flutter_appauth/ios/Classes/FlutterAppAuth.h +++ b/flutter_appauth/ios/Classes/FlutterAppAuth.h @@ -39,12 +39,12 @@ static NSString *const END_SESSION_ERROR_MESSAGE_FORMAT = @"Failed to end sessio @property(nonatomic, strong) NSString *discoveryUrl; @property(nonatomic, strong) NSDictionary *serviceConfigurationParameters; @property(nonatomic, strong) NSDictionary *additionalParameters; -@property(nonatomic, readwrite) BOOL preferEphemeralSession; +@property(nonatomic, strong) NSString *preferredExternalAgent; @end @interface AppAuthAuthorization : NSObject -- (id)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferEphemeralSession:(BOOL)preferEphemeralSession result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce; +- (id)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferredExternalAgent:(NSString*)preferredExternalAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce; - (id)performEndSessionRequest:(OIDServiceConfiguration *)serviceConfiguration requestParameters:(EndSessionRequestParameters *)requestParameters result:(FlutterResult)result; diff --git a/flutter_appauth/ios/Classes/FlutterAppAuth.m b/flutter_appauth/ios/Classes/FlutterAppAuth.m index 921ffbd3..94414c06 100644 --- a/flutter_appauth/ios/Classes/FlutterAppAuth.m +++ b/flutter_appauth/ios/Classes/FlutterAppAuth.m @@ -96,7 +96,7 @@ + (NSString *) formatMessageWithError:(NSString *)messageFormat error:(NSError * @implementation AppAuthAuthorization -- (id)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferEphemeralSession:(BOOL)preferEphemeralSession result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce { +- (id)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferredExternalAgent:(NSString*)preferredExternalAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce { return nil; } diff --git a/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m b/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m index c78bc067..006f6f81 100644 --- a/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m +++ b/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m @@ -28,7 +28,7 @@ @interface TokenRequestParameters : NSObject @property(nonatomic, strong) NSArray *scopes; @property(nonatomic, strong) NSDictionary *serviceConfigurationParameters; @property(nonatomic, strong) NSDictionary *additionalParameters; -@property(nonatomic, readwrite) BOOL preferEphemeralSession; +@property(nonatomic, strong) NSString *preferredExternalAgent; @end @@ -47,7 +47,7 @@ - (void)processArguments:(NSDictionary *)arguments { _scopes = [ArgumentProcessor processArgumentValue:arguments withKey:@"scopes"]; _serviceConfigurationParameters = [ArgumentProcessor processArgumentValue:arguments withKey:@"serviceConfiguration"]; _additionalParameters = [ArgumentProcessor processArgumentValue:arguments withKey:@"additionalParameters"]; - _preferEphemeralSession = [[ArgumentProcessor processArgumentValue:arguments withKey:@"preferEphemeralSession"] isEqual:@YES]; + _preferredExternalAgent = [ArgumentProcessor processArgumentValue:arguments withKey:@"preferredExternalAgent"]; } - (id)initWithArguments:(NSDictionary *)arguments { @@ -82,7 +82,7 @@ - (id)initWithArguments:(NSDictionary *)arguments { _discoveryUrl = [ArgumentProcessor processArgumentValue:arguments withKey:@"discoveryUrl"]; _serviceConfigurationParameters = [ArgumentProcessor processArgumentValue:arguments withKey:@"serviceConfiguration"]; _additionalParameters = [ArgumentProcessor processArgumentValue:arguments withKey:@"additionalParameters"]; - _preferEphemeralSession = [[ArgumentProcessor processArgumentValue:arguments withKey:@"preferEphemeralSession"] isEqual:@YES]; + _preferredExternalAgent = [ArgumentProcessor processArgumentValue:arguments withKey:@"preferredExternalAgent"]; return self; } @end @@ -149,7 +149,7 @@ -(void)handleAuthorizeMethodCall:(NSDictionary*)arguments result:(FlutterResult) if(requestParameters.serviceConfigurationParameters != nil) { OIDServiceConfiguration *serviceConfiguration = [self processServiceConfigurationParameters:requestParameters.serviceConfigurationParameters]; - _currentAuthorizationFlow = [authorization performAuthorization:serviceConfiguration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode nonce:requestParameters.nonce]; + _currentAuthorizationFlow = [authorization performAuthorization:serviceConfiguration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferredExternalAgent:requestParameters.preferredExternalAgent result:result exchangeCode:exchangeCode nonce:requestParameters.nonce]; } else if (requestParameters.discoveryUrl) { NSURL *discoveryUrl = [NSURL URLWithString:requestParameters.discoveryUrl]; [OIDAuthorizationService discoverServiceConfigurationForDiscoveryURL:discoveryUrl @@ -161,7 +161,7 @@ -(void)handleAuthorizeMethodCall:(NSDictionary*)arguments result:(FlutterResult) return; } - self->_currentAuthorizationFlow = [authorization performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode nonce:requestParameters.nonce]; + self->_currentAuthorizationFlow = [authorization performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferredExternalAgent:requestParameters.preferredExternalAgent result:result exchangeCode:exchangeCode nonce:requestParameters.nonce]; }]; } else { NSURL *issuerUrl = [NSURL URLWithString:requestParameters.issuer]; @@ -174,7 +174,7 @@ -(void)handleAuthorizeMethodCall:(NSDictionary*)arguments result:(FlutterResult) return; } - self->_currentAuthorizationFlow = [authorization performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode nonce:requestParameters.nonce]; + self->_currentAuthorizationFlow = [authorization performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferredExternalAgent:requestParameters.preferredExternalAgent result:result exchangeCode:exchangeCode nonce:requestParameters.nonce]; }]; } } diff --git a/flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.h b/flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.h new file mode 100644 index 00000000..cbf6dc87 --- /dev/null +++ b/flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.h @@ -0,0 +1,68 @@ +/*! @file OIDExternalUserAgentIOSSafariViewController.h + @brief AppAuth iOS SDK + @copyright + Copyright 2018 Google Inc. All Rights Reserved. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import + +#import "OIDExternalUserAgentIOSSafariViewController.h" +#import "OIDExternalUserAgent.h" +#import "OIDExternalUserAgentIOS.h" + +NS_ASSUME_NONNULL_BEGIN + +/*! @brief Allows library consumers to bootstrap an @c SFSafariViewController as they see fit. + @remarks Useful for customizing tint colors and presentation styles. + */ +@protocol OIDSafariViewControllerFactory + +/*! @brief Creates and returns a new @c SFSafariViewController. + @param URL The URL which the @c SFSafariViewController should load initially. + */ +- (SFSafariViewController *)safariViewControllerWithURL:(NSURL *)URL; + +@end + +/*! @brief A special-case iOS external user-agent that always uses + \SFSafariViewController (on iOS 9+). Most applications should use + the more generic @c OIDExternalUserAgentIOS to get the default + AppAuth user-agent handling with the benefits of Single Sign-on (SSO) + for all supported versions of iOS. + */ +@interface OIDExternalUserAgentIOSSafariViewController : NSObject + +/*! @brief Allows library consumers to change the @c OIDSafariViewControllerFactory used to create + new instances of @c SFSafariViewController. + @remarks Useful for customizing tint colors and presentation styles. + @param factory The @c OIDSafariViewControllerFactory to use for creating new instances of + @c SFSafariViewController. + */ ++ (void)setSafariViewControllerFactory:(id)factory; + +/*! @internal + @brief Unavailable. Please use @c initWithPresentingViewController: + */ +- (nonnull instancetype)init NS_UNAVAILABLE; + +/*! @brief The designated initializer. + @param presentingViewController The view controller from which to present the + \SFSafariViewController. + */ +- (nullable instancetype)initWithPresentingViewController: + (UIViewController *)presentingViewController + NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.m b/flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.m new file mode 100644 index 00000000..42ad8537 --- /dev/null +++ b/flutter_appauth/ios/Classes/OIDExternalUserAgentIOSSafariViewController.m @@ -0,0 +1,173 @@ +/*! @file OIDExternalUserAgentIOSSafariViewController.m + @brief AppAuth iOS SDK + @copyright + Copyright 2018 Google Inc. All Rights Reserved. + @copydetails + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "OIDExternalUserAgentIOSSafariViewController.h" + +#import + +#import "OIDErrorUtilities.h" +#import "OIDExternalUserAgentSession.h" +#import "OIDExternalUserAgentRequest.h" + +NS_ASSUME_NONNULL_BEGIN + +/** @brief The global/shared Safari view controller factory. Responsible for creating all new + instances of @c SFSafariViewController. + */ +static id __nullable gSafariViewControllerFactory; + +/** @brief The default @c OIDSafariViewControllerFactory which creates new instances of + @c SFSafariViewController using known best practices. + */ +@interface OIDDefaultSafariViewControllerFactory : NSObject +@end + +@interface OIDExternalUserAgentIOSSafariViewController () +@end + +@implementation OIDExternalUserAgentIOSSafariViewController { + UIViewController *_presentingViewController; + + BOOL _externalUserAgentFlowInProgress; + __weak id _session; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + __weak SFSafariViewController *_safariVC; +#pragma clang diagnostic pop +} + +/** @brief Obtains the current @c OIDSafariViewControllerFactory; creating a new default instance if + required. + */ ++ (id)safariViewControllerFactory { + if (!gSafariViewControllerFactory) { + gSafariViewControllerFactory = [[OIDDefaultSafariViewControllerFactory alloc] init]; + } + return gSafariViewControllerFactory; +} + ++ (void)setSafariViewControllerFactory:(id)factory { + NSAssert(factory, @"Parameter: |factory| must be non-nil."); + gSafariViewControllerFactory = factory; +} + +- (nullable instancetype)initWithPresentingViewController: + (UIViewController *)presentingViewController { + self = [super init]; + if (self) { + _presentingViewController = presentingViewController; + } + return self; +} + +- (BOOL)presentExternalUserAgentRequest:(id)request + session:(id)session { + if (_externalUserAgentFlowInProgress) { + // TODO: Handle errors as authorization is already in progress. + return NO; + } + + _externalUserAgentFlowInProgress = YES; + _session = session; + BOOL openedSafari = NO; + NSURL *requestURL = [request externalUserAgentRequestURL]; + + if (@available(iOS 9.0, *)) { + SFSafariViewController *safariVC = + [[[self class] safariViewControllerFactory] safariViewControllerWithURL:requestURL]; + safariVC.delegate = self; + safariVC.modalPresentationStyle = UIModalPresentationFormSheet; + _safariVC = safariVC; + [_presentingViewController presentViewController:safariVC animated:YES completion:nil]; + openedSafari = YES; + } else { + openedSafari = [[UIApplication sharedApplication] openURL:requestURL]; + } + + if (!openedSafari) { + [self cleanUp]; + NSError *safariError = [OIDErrorUtilities errorWithCode:OIDErrorCodeSafariOpenError + underlyingError:nil + description:@"Unable to open Safari."]; + [session failExternalUserAgentFlowWithError:safariError]; + } + return openedSafari; +} + +- (void)dismissExternalUserAgentAnimated:(BOOL)animated completion:(void (^)(void))completion { + if (!_externalUserAgentFlowInProgress) { + // Ignore this call if there is no authorization flow in progress. + return; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + SFSafariViewController *safariVC = _safariVC; +#pragma clang diagnostic pop + + [self cleanUp]; + + if (@available(iOS 9.0, *)) { + if (safariVC) { + [safariVC dismissViewControllerAnimated:YES completion:completion]; + } else { + if (completion) completion(); + } + } else { + if (completion) completion(); + } +} + +- (void)cleanUp { + // The weak references to |_safariVC| and |_session| are set to nil to avoid accidentally using + // them while not in an authorization flow. + _safariVC = nil; + _session = nil; + _externalUserAgentFlowInProgress = NO; +} + +#pragma mark - SFSafariViewControllerDelegate + +- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller NS_AVAILABLE_IOS(9.0) { + if (controller != _safariVC) { + // Ignore this call if the safari view controller do not match. + return; + } + if (!_externalUserAgentFlowInProgress) { + // Ignore this call if there is no authorization flow in progress. + return; + } + id session = _session; + [self cleanUp]; + NSError *error = [OIDErrorUtilities errorWithCode:OIDErrorCodeProgramCanceledAuthorizationFlow + underlyingError:nil + description:nil]; + [session failExternalUserAgentFlowWithError:error]; +} + +@end + +@implementation OIDDefaultSafariViewControllerFactory + +- (SFSafariViewController *)safariViewControllerWithURL:(NSURL *)URL NS_AVAILABLE_IOS(9.0) { + SFSafariViewController *safariViewController = + [[SFSafariViewController alloc] initWithURL:URL entersReaderIfAvailable:NO]; + return safariViewController; +} + +@end + +NS_ASSUME_NONNULL_END \ No newline at end of file diff --git a/flutter_appauth/lib/flutter_appauth.dart b/flutter_appauth/lib/flutter_appauth.dart index 480400e2..de2f4d78 100644 --- a/flutter_appauth/lib/flutter_appauth.dart +++ b/flutter_appauth/lib/flutter_appauth.dart @@ -7,6 +7,7 @@ export 'package:flutter_appauth_platform_interface/flutter_appauth_platform_inte AuthorizationTokenResponse, EndSessionRequest, EndSessionResponse, + ExternalAgentType, FlutterAppAuthOAuthError, FlutterAppAuthPlatformErrorDetails, FlutterAppAuthUserCancelledException, diff --git a/flutter_appauth/macos/Classes/AppAuthMacOSAuthorization.m b/flutter_appauth/macos/Classes/AppAuthMacOSAuthorization.m index 0d90ab0a..c6b94a24 100644 --- a/flutter_appauth/macos/Classes/AppAuthMacOSAuthorization.m +++ b/flutter_appauth/macos/Classes/AppAuthMacOSAuthorization.m @@ -2,7 +2,7 @@ @implementation AppAuthMacOSAuthorization -- (id)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferEphemeralSession:(BOOL)preferEphemeralSession result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce { +- (id)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration clientId:(NSString*)clientId clientSecret:(NSString*)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString*)redirectUrl additionalParameters:(NSDictionary *)additionalParameters preferredExternalAgent:(NSString*)preferredExternalAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode nonce:(NSString*)nonce { NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier]; NSString *codeChallenge = [OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier]; @@ -21,7 +21,7 @@ @implementation AppAuthMacOSAuthorization additionalParameters:additionalParameters]; NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; if(exchangeCode) { - NSObject *agent = [self userAgentWithPresentingWindow:keyWindow useEphemeralSession:preferEphemeralSession]; + NSObject *agent = [self userAgentWithPresentingWindow:keyWindow preferredExternalAgent:preferredExternalAgent]; return [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:agent callback:^(OIDAuthState *_Nullable authState, NSError *_Nullable error) { if(authState) { @@ -32,7 +32,7 @@ @implementation AppAuthMacOSAuthorization } }]; } else { - NSObject *agent = [self userAgentWithPresentingWindow:keyWindow useEphemeralSession:preferEphemeralSession]; + NSObject *agent = [self userAgentWithPresentingWindow:keyWindow preferredExternalAgent:preferredExternalAgent]; return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:agent callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) { if(authorizationResponse) { NSMutableDictionary *processedResponse = [[NSMutableDictionary alloc] init]; @@ -56,7 +56,7 @@ @implementation AppAuthMacOSAuthorization additionalParameters:requestParameters.additionalParameters]; NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; - id externalUserAgent = [self userAgentWithPresentingWindow:keyWindow useEphemeralSession:requestParameters.preferEphemeralSession]; + id externalUserAgent = [self userAgentWithPresentingWindow:keyWindow preferredExternalAgent:requestParameters.preferredExternalAgent]; return [OIDAuthorizationService presentEndSessionRequest:endSessionRequest externalUserAgent:externalUserAgent callback:^(OIDEndSessionResponse * _Nullable endSessionResponse, NSError * _Nullable error) { if(!endSessionResponse) { NSString *message = [NSString stringWithFormat:END_SESSION_ERROR_MESSAGE_FORMAT, [error localizedDescription]]; @@ -69,8 +69,8 @@ @implementation AppAuthMacOSAuthorization }]; } -- (id)userAgentWithPresentingWindow:(NSWindow *)presentingWindow useEphemeralSession:(BOOL)useEphemeralSession { - if (useEphemeralSession) { +- (id)userAgentWithPresentingWindow:(NSWindow *)presentingWindow preferredExternalAgent:(NSString*)preferredExternalAgent { + if ([preferredExternalAgent isEqual:@"ExternalAgentType.ephemeralAsWebAuthenticationSession"]) { return [[OIDExternalUserAgentMacNoSSO alloc] initWithPresentingWindow:presentingWindow]; } return [[OIDExternalUserAgentMac alloc] initWithPresentingWindow:presentingWindow];