diff --git a/flutter_appauth/README.md b/flutter_appauth/README.md index ad6b5e5d..e578435b 100644 --- a/flutter_appauth/README.md +++ b/flutter_appauth/README.md @@ -11,6 +11,7 @@ - [End session](#end-session) - [Handling errors](#handling-errors) - [Ephemeral Sessions (iOS and macOS only)](#ephemeral-sessions-ios-and-macos-only) + - [Using a proxy redirect URL](#using-a-proxy-redirect-url) - [Android setup](#android-setup) - [iOS/macOS setup](#iosmacos-setup) - [API docs](#api-docs) @@ -165,6 +166,64 @@ With an ephemeral session there will be no warning like `"app_name" Wants to Use The option `preferEphemeralSession = true` must only be used for the end session call if it is also used for the sign in call. Otherwise, there will be still an active login session in the browser. +### Using a proxy redirect URL + +Some identity providers only allow HTTPS redirect URIs and wil reject custom-scheme deep links (e.g. `com.example.myapp://oauth2redirect`) during the authorization request. A common solution is to host a small HTTPS endpoint that immediately redirects to your app's custom scheme. The `proxyRedirectUrl` parameter supports this pattern. + +**How it works:** + +1. Your app registers a custom-scheme redirect URL (e.g. `com.example.myapp://oauth2redirect`) with the OS so it can intercept the callback — this is `redirectUrl`. +2. You host an HTTPS endpoint (e.g. `https://myapp.example.com/oauth2redirect`) that simply redirects to the custom-scheme URL — this becomes `proxyRedirectUrl`. +3. When making the authorization request, `proxyRedirectUrl` is sent to the identity provider as the `redirect_uri` parameter so that it passes the provider's HTTPS validation. +4. The provider redirects the browser to `proxyRedirectUrl`, which in turn redirects to `redirectUrl`, and the OS hands the callback back to your app. +5. When exchanging the authorization code for tokens, `proxyRedirectUrl` must also be specified so that the `redirect_uri` in the token request matches what was used during authorization. + +**Example:** + +```dart +// Authorization + code exchange in one step +final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode( + AuthorizationTokenRequest( + '', + 'com.example.myapp://oauth2redirect', // redirectUrl: custom scheme the OS intercepts + proxyRedirectUrl: 'https://myapp.example.com/oauth2redirect', // sent to the provider as redirect_uri + discoveryUrl: '', + scopes: ['openid', 'profile', 'email', 'offline_access'], + ), +); +``` + +If you perform authorization and token exchange as separate steps, pass `proxyRedirectUrl` to both calls: + +```dart +// Step 1: authorize +final AuthorizationResponse authResult = await appAuth.authorize( + AuthorizationRequest( + '', + 'com.example.myapp://oauth2redirect', + proxyRedirectUrl: 'https://myapp.example.com/oauth2redirect', + discoveryUrl: '', + scopes: ['openid', 'profile', 'email', 'offline_access'], + ), +); + +// Step 2: exchange the code — proxyRedirectUrl must match what was sent during authorization +final TokenResponse tokenResult = await appAuth.token( + TokenRequest( + '', + 'com.example.myapp://oauth2redirect', + proxyRedirectUrl: 'https://myapp.example.com/oauth2redirect', + authorizationCode: authResult.authorizationCode, + discoveryUrl: '', + codeVerifier: authResult.codeVerifier, + nonce: authResult.nonce, + scopes: ['openid', 'profile', 'email', 'offline_access'], + ), +); +``` + +> **Note:** The `redirectUrl` still needs to be registered with the OS (via `Info.plist` on iOS/macOS and `build.gradle`/`AndroidManifest.xml` on Android) so the OS knows to route the deep link back to your app. Only `proxyRedirectUrl` is sent to the identity provider. + ## Android setup Go to the `build.gradle.kts` file for your Android app to specify the custom scheme so that there should be a section in it that look similar to the following but replace `` with the desired value: diff --git a/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java b/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java index a24b4894..7a7fe390 100644 --- a/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java +++ b/flutter_appauth/android/src/main/java/io/crossingthestreams/flutterappauth/FlutterAppauthPlugin.java @@ -210,6 +210,7 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu final String issuer = (String) arguments.get("issuer"); final String discoveryUrl = (String) arguments.get("discoveryUrl"); final String redirectUrl = (String) arguments.get("redirectUrl"); + final String proxyRedirectUrl = (String) arguments.get("proxyRedirectUrl"); final String loginHint = (String) arguments.get("loginHint"); final String nonce = (String) arguments.get("nonce"); clientSecret = (String) arguments.get("clientSecret"); @@ -228,6 +229,7 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu discoveryUrl, scopes, redirectUrl, + proxyRedirectUrl, serviceConfigurationParameters, additionalParameters, loginHint, @@ -266,12 +268,15 @@ private TokenRequestParameters processTokenRequestArguments(Map final Map additionalParameters = (Map) arguments.get("additionalParameters"); allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); + final String proxyRedirectUrl = (String) arguments.get("proxyRedirectUrl"); + final String effectiveRedirectUrl = proxyRedirectUrl != null ? proxyRedirectUrl : redirectUrl; return new TokenRequestParameters( clientId, issuer, discoveryUrl, scopes, - redirectUrl, + effectiveRedirectUrl, + null, refreshToken, authorizationCode, codeVerifier, @@ -317,6 +322,7 @@ private void handleAuthorizeMethodCall( serviceConfiguration, tokenRequestParameters.clientId, tokenRequestParameters.redirectUrl, + tokenRequestParameters.proxyRedirectUrl, tokenRequestParameters.scopes, tokenRequestParameters.loginHint, tokenRequestParameters.nonce, @@ -336,6 +342,7 @@ public void onFetchConfigurationCompleted( serviceConfiguration, tokenRequestParameters.clientId, tokenRequestParameters.redirectUrl, + tokenRequestParameters.proxyRedirectUrl, tokenRequestParameters.scopes, tokenRequestParameters.loginHint, tokenRequestParameters.nonce, @@ -409,6 +416,7 @@ private void performAuthorization( AuthorizationServiceConfiguration serviceConfiguration, String clientId, String redirectUrl, + String proxyRedirectUrl, ArrayList scopes, String loginHint, String nonce, @@ -416,9 +424,10 @@ private void performAuthorization( boolean exchangeCode, ArrayList promptValues, String responseMode) { + String effectiveRedirectUrl = proxyRedirectUrl != null ? proxyRedirectUrl : redirectUrl; AuthorizationRequest.Builder authRequestBuilder = new AuthorizationRequest.Builder( - serviceConfiguration, clientId, ResponseTypeValues.CODE, Uri.parse(redirectUrl)); + serviceConfiguration, clientId, ResponseTypeValues.CODE, Uri.parse(effectiveRedirectUrl)); if (scopes != null && !scopes.isEmpty()) { authRequestBuilder.setScopes(scopes); @@ -791,6 +800,7 @@ private class TokenRequestParameters { final String discoveryUrl; final ArrayList scopes; final String redirectUrl; + final String proxyRedirectUrl; final String refreshToken; final String grantType; final String codeVerifier; @@ -805,6 +815,7 @@ private TokenRequestParameters( String discoveryUrl, ArrayList scopes, String redirectUrl, + String proxyRedirectUrl, String refreshToken, String authorizationCode, String codeVerifier, @@ -817,6 +828,7 @@ private TokenRequestParameters( this.discoveryUrl = discoveryUrl; this.scopes = scopes; this.redirectUrl = redirectUrl; + this.proxyRedirectUrl = proxyRedirectUrl; this.refreshToken = refreshToken; this.authorizationCode = authorizationCode; this.codeVerifier = codeVerifier; @@ -868,6 +880,7 @@ private AuthorizationTokenRequestParameters( String discoveryUrl, ArrayList scopes, String redirectUrl, + String proxyRedirectUrl, Map serviceConfigurationParameters, Map additionalParameters, String loginHint, @@ -880,6 +893,7 @@ private AuthorizationTokenRequestParameters( discoveryUrl, scopes, redirectUrl, + proxyRedirectUrl, null, null, null, diff --git a/flutter_appauth/example/lib/main.dart b/flutter_appauth/example/lib/main.dart index a47b0893..a1c9237b 100644 --- a/flutter_appauth/example/lib/main.dart +++ b/flutter_appauth/example/lib/main.dart @@ -43,6 +43,8 @@ class _MyAppState extends State { // For a list of client IDs, go to https://demo.duendesoftware.com final String _clientId = 'interactive.public'; final String _redirectUrl = 'com.duendesoftware.demo:/oauthredirect'; + final String? _proxyRedirectUrl = + null /*'https://my-server.com/auth/redirect?client=app'*/; final String _issuer = 'https://demo.duendesoftware.com'; final String _discoveryUrl = 'https://demo.duendesoftware.com/.well-known/openid-configuration'; @@ -240,7 +242,11 @@ class _MyAppState extends State { _setBusyState(); final TokenResponse result = await _appAuth.token(TokenRequest( _clientId, _redirectUrl, - refreshToken: _refreshToken, issuer: _issuer, scopes: _scopes)); + proxyRedirectUrl: _proxyRedirectUrl, + refreshToken: _refreshToken, + serviceConfiguration: _serviceConfiguration, + issuer: _issuer, + scopes: _scopes)); _processTokenResponse(result); await _testApi(result); } catch (e) { @@ -255,6 +261,7 @@ class _MyAppState extends State { _setBusyState(); final TokenResponse result = await _appAuth.token(TokenRequest( _clientId, _redirectUrl, + proxyRedirectUrl: _proxyRedirectUrl, authorizationCode: _authorizationCode, discoveryUrl: _discoveryUrl, codeVerifier: _codeVerifier, @@ -272,9 +279,9 @@ class _MyAppState extends State { Future _signInWithNoCodeExchange() async { try { _setBusyState(); - /* + /* The discovery endpoint (_discoveryUrl) is used to find the - configuration. The code challenge generation can be checked here + configuration. The code challenge generation can be checked here > https://github.com/MaikuB/flutter_appauth/search?q=challenge. The code challenge is generated from the code verifier and only the code verifier is included in the result. This because to get the token @@ -285,15 +292,19 @@ class _MyAppState extends State { */ final AuthorizationResponse result = await _appAuth.authorize( AuthorizationRequest(_clientId, _redirectUrl, - discoveryUrl: _discoveryUrl, scopes: _scopes, loginHint: 'bob'), + proxyRedirectUrl: _proxyRedirectUrl, + discoveryUrl: _discoveryUrl, + scopes: _scopes, + loginHint: 'bob'), ); - /* + /* or just use the issuer var result = await _appAuth.authorize( AuthorizationRequest( _clientId, _redirectUrl, + proxyRedirectUrl: _proxyRedirectUrl, issuer: _issuer, scopes: _scopes, ), @@ -317,6 +328,7 @@ class _MyAppState extends State { // use the discovery endpoint to find the configuration final AuthorizationResponse result = await _appAuth.authorize( AuthorizationRequest(_clientId, _redirectUrl, + proxyRedirectUrl: _proxyRedirectUrl, discoveryUrl: _discoveryUrl, scopes: _scopes, loginHint: 'bob', @@ -344,6 +356,7 @@ class _MyAppState extends State { final AuthorizationTokenResponse result = await _appAuth.authorizeAndExchangeCode( AuthorizationTokenRequest(_clientId, _redirectUrl, + proxyRedirectUrl: _proxyRedirectUrl, serviceConfiguration: _serviceConfiguration, scopes: _scopes, externalUserAgent: externalUserAgent), @@ -359,6 +372,7 @@ class _MyAppState extends State { final AuthorizationTokenResponse result = await _appAuth .authorizeAndExchangeCode( AuthorizationTokenRequest(_clientId, _redirectUrl, + proxyRedirectUrl: _proxyRedirectUrl, serviceConfiguration: _serviceConfiguration, scopes: _scopes, promptValues: ['login']), @@ -411,11 +425,13 @@ class _MyAppState extends State { void _processAuthTokenResponse(AuthorizationTokenResponse response) { setState(() { - _accessToken = _accessTokenTextController.text = response.accessToken!; - _idToken = _idTokenTextController.text = response.idToken!; - _refreshToken = _refreshTokenTextController.text = response.refreshToken!; + _accessToken = + _accessTokenTextController.text = response.accessToken ?? ''; + _idToken = _idTokenTextController.text = response.idToken ?? ''; + _refreshToken = + _refreshTokenTextController.text = response.refreshToken ?? ''; _accessTokenExpirationTextController.text = - response.accessTokenExpirationDateTime!.toIso8601String(); + response.accessTokenExpirationDateTime?.toIso8601String() ?? ''; }); } diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m index 32e8c19b..ce7dbdeb 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m @@ -1,4 +1,5 @@ #import "AppAuthIOSAuthorization.h" +#import "FlutterAppAuthProxyUserAgent.h" @implementation AppAuthIOSAuthorization @@ -8,6 +9,7 @@ @implementation AppAuthIOSAuthorization clientSecret:(NSString *)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString *)redirectUrl + proxyRedirectUrl:(NSString *)proxyRedirectUrl additionalParameters:(NSDictionary *)additionalParameters externalUserAgent:(NSNumber *)externalUserAgent result:(FlutterResult)result @@ -16,13 +18,14 @@ @implementation AppAuthIOSAuthorization NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier]; NSString *codeChallenge = [OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier]; + NSString *effectiveRedirectUrl = proxyRedirectUrl ?: redirectUrl; OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:serviceConfiguration clientId:clientId clientSecret:clientSecret scope:[OIDScopeUtilities scopesWithArray:scopes] - redirectURL:[NSURL URLWithString:redirectUrl] + redirectURL:[NSURL URLWithString:effectiveRedirectUrl] responseType:OIDResponseTypeCode state:[OIDAuthorizationRequest generateState] nonce:nonce != nil @@ -36,7 +39,9 @@ @implementation AppAuthIOSAuthorization if (exchangeCode) { id agent = [self userAgentWithViewController:rootViewController - externalUserAgent:externalUserAgent]; + externalUserAgent:externalUserAgent + redirectUrl:redirectUrl + proxyRedirectUrl:proxyRedirectUrl]; return [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:agent @@ -68,7 +73,9 @@ @implementation AppAuthIOSAuthorization } else { id agent = [self userAgentWithViewController:rootViewController - externalUserAgent:externalUserAgent]; + externalUserAgent:externalUserAgent + redirectUrl:redirectUrl + proxyRedirectUrl:proxyRedirectUrl]; return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:agent @@ -136,7 +143,9 @@ @implementation AppAuthIOSAuthorization UIViewController *rootViewController = [self rootViewController]; id externalUserAgent = [self userAgentWithViewController:rootViewController - externalUserAgent:requestParameters.externalUserAgent]; + externalUserAgent:requestParameters.externalUserAgent + redirectUrl:@"" + proxyRedirectUrl:nil]; return [OIDAuthorizationService presentEndSessionRequest:endSessionRequest @@ -164,12 +173,25 @@ @implementation AppAuthIOSAuthorization - (id) userAgentWithViewController:(UIViewController *)rootViewController - externalUserAgent:(NSNumber *)externalUserAgent { - if ([externalUserAgent integerValue] == EphemeralASWebAuthenticationSession) { + externalUserAgent:(NSNumber *)externalUserAgent + redirectUrl:(NSString *)redirectUrl + proxyRedirectUrl:(NSString *_Nullable)proxyRedirectUrl { + NSInteger agentValue = [externalUserAgent integerValue]; + if (proxyRedirectUrl && + (agentValue == ASWebAuthenticationSession || + agentValue == EphemeralASWebAuthenticationSession)) { + NSString *callbackScheme = [NSURL URLWithString:redirectUrl].scheme; + return [[FlutterAppAuthProxyUserAgent alloc] + initWithPresentingViewController:rootViewController + callbackScheme:callbackScheme + proxyRedirectUrl:proxyRedirectUrl + ephemeral:(agentValue == EphemeralASWebAuthenticationSession)]; + } + if (agentValue == EphemeralASWebAuthenticationSession) { return [[OIDExternalUserAgentIOSNoSSO alloc] initWithPresentingViewController:rootViewController]; } - if ([externalUserAgent integerValue] == SafariViewController) { + if (agentValue == SafariViewController) { return [[OIDExternalUserAgentIOSSafariViewController alloc] initWithPresentingViewController:rootViewController]; } diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h index 5683ab88..5488ae3b 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h @@ -72,6 +72,7 @@ typedef NS_ENUM(NSInteger, ExternalUserAgent) { clientSecret:(NSString *)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString *)redirectUrl + proxyRedirectUrl:(NSString *_Nullable)proxyRedirectUrl additionalParameters:(NSDictionary *)additionalParameters externalUserAgent:(NSNumber *)externalUserAgent result:(FlutterResult)result diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthProxyUserAgent.h b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthProxyUserAgent.h new file mode 100644 index 00000000..ce5b6f35 --- /dev/null +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthProxyUserAgent.h @@ -0,0 +1,32 @@ +#import +#import + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +/// A custom OIDExternalUserAgent for the proxy redirect URL use case. +/// +/// When an OAuth server only allows HTTPS redirect URIs but the app uses a +/// custom-scheme deep link, this user agent: +/// 1. Starts an ASWebAuthenticationSession with `callbackURLScheme` set to +/// the custom scheme (so the OS intercepts the deep link). +/// 2. Rewrites the returned custom-scheme callback URL to use the +/// proxyRedirectUrl's base before passing it to AppAuth, so AppAuth's +/// validation (which expects the proxy https URL) succeeds. +@interface FlutterAppAuthProxyUserAgent + : NSObject + +- (instancetype)initWithPresentingViewController: + (UIViewController *)presentingViewController + callbackScheme:(NSString *)callbackScheme + proxyRedirectUrl:(NSString *)proxyRedirectUrl + ephemeral:(BOOL)ephemeral; + +@end + +NS_ASSUME_NONNULL_END diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthProxyUserAgent.m b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthProxyUserAgent.m new file mode 100644 index 00000000..168a812d --- /dev/null +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthProxyUserAgent.m @@ -0,0 +1,135 @@ +#import "FlutterAppAuthProxyUserAgent.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +@interface FlutterAppAuthProxyUserAgent () + +@end +#endif + +@implementation FlutterAppAuthProxyUserAgent { + UIViewController *_presentingViewController; + NSString *_callbackScheme; + NSString *_proxyRedirectUrl; + BOOL _ephemeral; + + BOOL _externalUserAgentFlowInProgress; + __weak id _session; + ASWebAuthenticationSession *_webAuthenticationVC; +} + +- (instancetype)initWithPresentingViewController: + (UIViewController *)presentingViewController + callbackScheme:(NSString *)callbackScheme + proxyRedirectUrl:(NSString *)proxyRedirectUrl + ephemeral:(BOOL)ephemeral { + self = [super init]; + if (self) { + _presentingViewController = presentingViewController; + _callbackScheme = callbackScheme; + _proxyRedirectUrl = proxyRedirectUrl; + _ephemeral = ephemeral; + } + return self; +} + +- (BOOL)presentExternalUserAgentRequest:(id)request + session:(id)session { + if (_externalUserAgentFlowInProgress) { + return NO; + } + + _externalUserAgentFlowInProgress = YES; + _session = session; + + NSURL *requestURL = [request externalUserAgentRequestURL]; + NSString *callbackScheme = _callbackScheme; + NSString *proxyRedirectUrl = _proxyRedirectUrl; + + __weak FlutterAppAuthProxyUserAgent *weakSelf = self; + ASWebAuthenticationSession *authenticationVC = [[ASWebAuthenticationSession alloc] + initWithURL:requestURL + callbackURLScheme:callbackScheme + completionHandler:^(NSURL *_Nullable callbackURL, NSError *_Nullable error) { + __strong FlutterAppAuthProxyUserAgent *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + strongSelf->_webAuthenticationVC = nil; + if (callbackURL) { + // Rewrite the custom-scheme callback URL to use proxyRedirectUrl's + // base so AppAuth's redirect URI validation passes. + NSURLComponents *proxyComponents = + [NSURLComponents componentsWithString:proxyRedirectUrl]; + NSURLComponents *incomingComponents = + [NSURLComponents componentsWithURL:callbackURL + resolvingAgainstBaseURL:NO]; + proxyComponents.queryItems = incomingComponents.queryItems; + NSURL *rewrittenUrl = [proxyComponents URL]; + [strongSelf->_session resumeExternalUserAgentFlowWithURL:rewrittenUrl]; + } else { + NSError *agentError = [OIDErrorUtilities + errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow + underlyingError:error + description:nil]; + [strongSelf->_session failExternalUserAgentFlowWithError:agentError]; + } + }]; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 + if (@available(iOS 13.0, *)) { + authenticationVC.presentationContextProvider = self; + if (_ephemeral) { + authenticationVC.prefersEphemeralWebBrowserSession = YES; + } + } +#endif + + _webAuthenticationVC = authenticationVC; + BOOL started = [authenticationVC start]; + if (!started) { + [self cleanUp]; + NSError *startError = [OIDErrorUtilities + errorWithCode:OIDErrorCodeSafariOpenError + underlyingError:nil + description:@"Unable to start ASWebAuthenticationSession."]; + [session failExternalUserAgentFlowWithError:startError]; + } + return started; +} + +- (void)dismissExternalUserAgentAnimated:(BOOL)animated + completion:(void (^)(void))completion { + if (!_externalUserAgentFlowInProgress) { + if (completion) completion(); + return; + } + ASWebAuthenticationSession *webAuthenticationVC = _webAuthenticationVC; + [self cleanUp]; + if (webAuthenticationVC) { + [webAuthenticationVC cancel]; + } + if (completion) completion(); +} + +- (void)cleanUp { + _webAuthenticationVC = nil; + _session = nil; + _externalUserAgentFlowInProgress = NO; +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +#pragma mark - ASWebAuthenticationPresentationContextProviding + +- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession: + (ASWebAuthenticationSession *)session API_AVAILABLE(ios(13.0)) { + return _presentingViewController.view.window; +} +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m index 4d41ea90..bce706f7 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m @@ -31,6 +31,7 @@ @interface TokenRequestParameters : NSObject @property(nonatomic, strong) NSDictionary *serviceConfigurationParameters; @property(nonatomic, strong) NSDictionary *additionalParameters; @property(nonatomic, strong) NSNumber *externalUserAgent; +@property(nonatomic, strong) NSString *proxyRedirectUrl; @end @@ -67,6 +68,9 @@ - (void)processArguments:(NSDictionary *)arguments { _externalUserAgent = [ArgumentProcessor processArgumentValue:arguments withKey:@"externalUserAgent"]; + _proxyRedirectUrl = + [ArgumentProcessor processArgumentValue:arguments + withKey:@"proxyRedirectUrl"]; } - (id)initWithArguments:(NSDictionary *)arguments { @@ -197,6 +201,16 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments forKey:@"response_mode"]; } + if (requestParameters.proxyRedirectUrl) { + NSInteger agentValue = [requestParameters.externalUserAgent integerValue]; + // ASWebAuthenticationSession cases are handled by FlutterAppAuthProxyUserAgent internally. + // Only set _pendingProxyRedirectUrl for SafariViewController and browser user agents, + // which rely on openURL:/scene:openURLContexts: for the callback. + if (agentValue != ASWebAuthenticationSession && + agentValue != EphemeralASWebAuthenticationSession) { + _pendingProxyRedirectUrl = requestParameters.proxyRedirectUrl; + } + } if (requestParameters.serviceConfigurationParameters != nil) { OIDServiceConfiguration *serviceConfiguration = [self processServiceConfigurationParameters: @@ -207,6 +221,7 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl + proxyRedirectUrl:requestParameters.proxyRedirectUrl additionalParameters:requestParameters.additionalParameters externalUserAgent:requestParameters.externalUserAgent result:result @@ -243,6 +258,9 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments redirectUrl: requestParameters .redirectUrl + proxyRedirectUrl: + requestParameters + .proxyRedirectUrl additionalParameters: requestParameters .additionalParameters @@ -283,6 +301,9 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments redirectUrl: requestParameters .redirectUrl + proxyRedirectUrl: + requestParameters + .proxyRedirectUrl additionalParameters: requestParameters .additionalParameters @@ -449,11 +470,12 @@ - (void)handleEndSessionMethodCall:(NSDictionary *)arguments - (void)performTokenRequest:(OIDServiceConfiguration *)serviceConfiguration requestParameters:(TokenRequestParameters *)requestParameters result:(FlutterResult)result { + NSString *effectiveRedirectUrl = requestParameters.proxyRedirectUrl ?: requestParameters.redirectUrl; OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] initWithConfiguration:serviceConfiguration grantType:requestParameters.grantType authorizationCode:requestParameters.authorizationCode - redirectURL:[NSURL URLWithString:requestParameters.redirectUrl] + redirectURL:[NSURL URLWithString:effectiveRedirectUrl] clientID:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes @@ -480,11 +502,25 @@ - (void)performTokenRequest:(OIDServiceConfiguration *)serviceConfiguration } #if TARGET_OS_IOS +- (NSURL *)rewriteUrlForProxyIfNeeded:(NSURL *)url { + if (_pendingProxyRedirectUrl) { + NSURLComponents *proxyComponents = + [NSURLComponents componentsWithString:_pendingProxyRedirectUrl]; + NSURLComponents *incomingComponents = + [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + proxyComponents.queryItems = incomingComponents.queryItems; + _pendingProxyRedirectUrl = nil; + return [proxyComponents URL]; + } + return url; +} + - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options: (NSDictionary *)options { - if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:url]) { + NSURL *effectiveUrl = [self rewriteUrlForProxyIfNeeded:url]; + if ([_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:effectiveUrl]) { _currentAuthorizationFlow = nil; return YES; } @@ -502,8 +538,9 @@ - (BOOL)application:(UIApplication *)application - (BOOL)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts { for (UIOpenURLContext *URLContext in URLContexts) { + NSURL *effectiveUrl = [self rewriteUrlForProxyIfNeeded:URLContext.URL]; if ([_currentAuthorizationFlow - resumeExternalUserAgentFlowWithURL:URLContext.URL]) { + resumeExternalUserAgentFlowWithURL:effectiveUrl]) { _currentAuthorizationFlow = nil; return YES; } diff --git a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin_Private.h b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin_Private.h index a7f54e31..4b53517d 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin_Private.h +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin_Private.h @@ -19,5 +19,6 @@ @property(nonatomic, strong, nullable) id currentAuthorizationFlow; +@property(nonatomic, strong, nullable) NSString *pendingProxyRedirectUrl; @end diff --git a/flutter_appauth/lib/src/flutter_appauth.dart b/flutter_appauth/lib/src/flutter_appauth.dart index 1b326325..b52bfadd 100644 --- a/flutter_appauth/lib/src/flutter_appauth.dart +++ b/flutter_appauth/lib/src/flutter_appauth.dart @@ -5,7 +5,8 @@ class FlutterAppAuth { /// Convenience method for authorizing and then exchanges code Future authorizeAndExchangeCode( - AuthorizationTokenRequest request) { + AuthorizationTokenRequest request, + ) { return FlutterAppAuthPlatform.instance.authorizeAndExchangeCode(request); } diff --git a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m index 30a4694c..35a89c03 100644 --- a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m @@ -1,4 +1,5 @@ #import "AppAuthMacOSAuthorization.h" +#import "FlutterAppAuthMacProxyUserAgent.h" @implementation AppAuthMacOSAuthorization @@ -8,6 +9,7 @@ @implementation AppAuthMacOSAuthorization clientSecret:(NSString *)clientSecret scopes:(NSArray *)scopes redirectUrl:(NSString *)redirectUrl + proxyRedirectUrl:(NSString *)proxyRedirectUrl additionalParameters:(NSDictionary *)additionalParameters externalUserAgent:(NSNumber *)externalUserAgent result:(FlutterResult)result @@ -16,13 +18,14 @@ @implementation AppAuthMacOSAuthorization NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier]; NSString *codeChallenge = [OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier]; + NSString *effectiveRedirectUrl = proxyRedirectUrl ?: redirectUrl; OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:serviceConfiguration clientId:clientId clientSecret:clientSecret scope:[OIDScopeUtilities scopesWithArray:scopes] - redirectURL:[NSURL URLWithString:redirectUrl] + redirectURL:[NSURL URLWithString:effectiveRedirectUrl] responseType:OIDResponseTypeCode state:[OIDAuthorizationRequest generateState] nonce:nonce != nil @@ -36,7 +39,9 @@ @implementation AppAuthMacOSAuthorization if (exchangeCode) { NSObject *agent = [self userAgentWithPresentingWindow:keyWindow - externalUserAgent:externalUserAgent]; + externalUserAgent:externalUserAgent + redirectUrl:redirectUrl + proxyRedirectUrl:proxyRedirectUrl]; return [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:agent @@ -68,7 +73,9 @@ @implementation AppAuthMacOSAuthorization } else { NSObject *agent = [self userAgentWithPresentingWindow:keyWindow - externalUserAgent:externalUserAgent]; + externalUserAgent:externalUserAgent + redirectUrl:redirectUrl + proxyRedirectUrl:proxyRedirectUrl]; return [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:agent @@ -136,7 +143,9 @@ @implementation AppAuthMacOSAuthorization NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; id externalUserAgent = [self userAgentWithPresentingWindow:keyWindow - externalUserAgent:requestParameters.externalUserAgent]; + externalUserAgent:requestParameters.externalUserAgent + redirectUrl:@"" + proxyRedirectUrl:nil]; return [OIDAuthorizationService presentEndSessionRequest:endSessionRequest externalUserAgent:externalUserAgent @@ -163,8 +172,21 @@ @implementation AppAuthMacOSAuthorization - (id) userAgentWithPresentingWindow:(NSWindow *)presentingWindow - externalUserAgent:(NSNumber *)externalUserAgent { - if ([externalUserAgent integerValue] == EphemeralASWebAuthenticationSession) { + externalUserAgent:(NSNumber *)externalUserAgent + redirectUrl:(NSString *)redirectUrl + proxyRedirectUrl:(NSString *_Nullable)proxyRedirectUrl { + NSInteger agentValue = [externalUserAgent integerValue]; + if (proxyRedirectUrl && + (agentValue == ASWebAuthenticationSession || + agentValue == EphemeralASWebAuthenticationSession)) { + NSString *callbackScheme = [NSURL URLWithString:redirectUrl].scheme; + return [[FlutterAppAuthMacProxyUserAgent alloc] + initWithPresentingWindow:presentingWindow + callbackScheme:callbackScheme + proxyRedirectUrl:proxyRedirectUrl + ephemeral:(agentValue == EphemeralASWebAuthenticationSession)]; + } + if (agentValue == EphemeralASWebAuthenticationSession) { return [[OIDExternalUserAgentMacNoSSO alloc] initWithPresentingWindow:presentingWindow]; } diff --git a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthMacProxyUserAgent.h b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthMacProxyUserAgent.h new file mode 100644 index 00000000..65577fac --- /dev/null +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthMacProxyUserAgent.h @@ -0,0 +1,26 @@ +#import +#import + +#ifdef SWIFT_PACKAGE +@import AppAuth; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +/// A macOS-specific OIDExternalUserAgent for the proxy redirect URL use case. +/// +/// Mirrors FlutterAppAuthProxyUserAgent (iOS) but uses NSWindow for the +/// ASWebAuthenticationSession presentation context. +@interface FlutterAppAuthMacProxyUserAgent + : NSObject + +- (instancetype)initWithPresentingWindow:(NSWindow *)presentingWindow + callbackScheme:(NSString *)callbackScheme + proxyRedirectUrl:(NSString *)proxyRedirectUrl + ephemeral:(BOOL)ephemeral; + +@end + +NS_ASSUME_NONNULL_END diff --git a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthMacProxyUserAgent.m b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthMacProxyUserAgent.m new file mode 100644 index 00000000..3f8cdfaa --- /dev/null +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppAuthMacProxyUserAgent.m @@ -0,0 +1,148 @@ +#import "FlutterAppAuthMacProxyUserAgent.h" + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FlutterAppAuthMacProxyUserAgent () + +@end + +@implementation FlutterAppAuthMacProxyUserAgent { + NSWindow *_presentingWindow; + NSString *_callbackScheme; + NSString *_proxyRedirectUrl; + BOOL _ephemeral; + + BOOL _externalUserAgentFlowInProgress; + __weak id _session; + ASWebAuthenticationSession *_webAuthenticationSession; +} + +- (instancetype)initWithPresentingWindow:(NSWindow *)presentingWindow + callbackScheme:(NSString *)callbackScheme + proxyRedirectUrl:(NSString *)proxyRedirectUrl + ephemeral:(BOOL)ephemeral { + self = [super init]; + if (self) { + _presentingWindow = presentingWindow; + _callbackScheme = callbackScheme; + _proxyRedirectUrl = proxyRedirectUrl; + _ephemeral = ephemeral; + } + return self; +} + +- (BOOL)presentExternalUserAgentRequest:(id)request + session:(id)session { + if (_externalUserAgentFlowInProgress) { + return NO; + } + + _externalUserAgentFlowInProgress = YES; + _session = session; + + NSURL *requestURL = [request externalUserAgentRequestURL]; + NSString *callbackScheme = _callbackScheme; + NSString *proxyRedirectUrl = _proxyRedirectUrl; + + if (@available(macOS 10.15, *)) { + if (_presentingWindow) { + __weak FlutterAppAuthMacProxyUserAgent *weakSelf = self; + ASWebAuthenticationSession *authenticationSession = + [[ASWebAuthenticationSession alloc] + initWithURL:requestURL + callbackURLScheme:callbackScheme + completionHandler:^(NSURL *_Nullable callbackURL, + NSError *_Nullable error) { + __strong FlutterAppAuthMacProxyUserAgent *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + strongSelf->_webAuthenticationSession = nil; + if (callbackURL) { + // Rewrite the custom-scheme callback URL to use + // proxyRedirectUrl's base so AppAuth's redirect URI + // validation passes. + NSURLComponents *proxyComponents = + [NSURLComponents componentsWithString:proxyRedirectUrl]; + NSURLComponents *incomingComponents = + [NSURLComponents componentsWithURL:callbackURL + resolvingAgainstBaseURL:NO]; + proxyComponents.queryItems = incomingComponents.queryItems; + NSURL *rewrittenUrl = [proxyComponents URL]; + [strongSelf->_session + resumeExternalUserAgentFlowWithURL:rewrittenUrl]; + } else { + NSError *agentError = [OIDErrorUtilities + errorWithCode:OIDErrorCodeUserCanceledAuthorizationFlow + underlyingError:error + description:nil]; + [strongSelf->_session + failExternalUserAgentFlowWithError:agentError]; + } + }]; + + authenticationSession.presentationContextProvider = self; + if (_ephemeral) { + authenticationSession.prefersEphemeralWebBrowserSession = YES; + } + _webAuthenticationSession = authenticationSession; + BOOL started = [authenticationSession start]; + if (!started) { + [self cleanUp]; + NSError *startError = [OIDErrorUtilities + errorWithCode:OIDErrorCodeSafariOpenError + underlyingError:nil + description:@"Unable to start ASWebAuthenticationSession."]; + [session failExternalUserAgentFlowWithError:startError]; + } + return started; + } + } + + // Fallback: open system browser (macOS < 10.15 or no presenting window). + // URL rewriting for the callback is handled in handleGetURLEvent: via + // _pendingProxyRedirectUrl. + BOOL openedBrowser = [[NSWorkspace sharedWorkspace] openURL:requestURL]; + if (!openedBrowser) { + [self cleanUp]; + NSError *browserError = + [OIDErrorUtilities errorWithCode:OIDErrorCodeBrowserOpenError + underlyingError:nil + description:@"Unable to open the browser."]; + [session failExternalUserAgentFlowWithError:browserError]; + } + return openedBrowser; +} + +- (void)dismissExternalUserAgentAnimated:(BOOL)animated + completion:(void (^)(void))completion { + if (!_externalUserAgentFlowInProgress) { + if (completion) completion(); + return; + } + ASWebAuthenticationSession *webAuthenticationSession = _webAuthenticationSession; + [self cleanUp]; + if (webAuthenticationSession) { + [webAuthenticationSession cancel]; + } + if (completion) completion(); +} + +- (void)cleanUp { + _webAuthenticationSession = nil; + _session = nil; + _externalUserAgentFlowInProgress = NO; +} + +#pragma mark - ASWebAuthenticationPresentationContextProviding + +- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession: + (ASWebAuthenticationSession *)session API_AVAILABLE(macosx(10.15)) { + return _presentingWindow; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m index cc36cc1c..7f058ec1 100644 --- a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m @@ -31,6 +31,7 @@ @interface TokenRequestParameters : NSObject @property(nonatomic, strong) NSDictionary *serviceConfigurationParameters; @property(nonatomic, strong) NSDictionary *additionalParameters; @property(nonatomic, strong) NSNumber *externalUserAgent; +@property(nonatomic, strong) NSString *proxyRedirectUrl; @end @@ -67,6 +68,9 @@ - (void)processArguments:(NSDictionary *)arguments { _externalUserAgent = [ArgumentProcessor processArgumentValue:arguments withKey:@"externalUserAgent"]; + _proxyRedirectUrl = + [ArgumentProcessor processArgumentValue:arguments + withKey:@"proxyRedirectUrl"]; } - (id)initWithArguments:(NSDictionary *)arguments { @@ -196,6 +200,15 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments forKey:@"response_mode"]; } + if (requestParameters.proxyRedirectUrl) { + NSInteger agentValue = [requestParameters.externalUserAgent integerValue]; + // ASWebAuthenticationSession cases are handled by FlutterAppAuthMacProxyUserAgent. + // For the system-browser fallback, handleGetURLEvent: needs _pendingProxyRedirectUrl. + if (agentValue != ASWebAuthenticationSession && + agentValue != EphemeralASWebAuthenticationSession) { + _pendingProxyRedirectUrl = requestParameters.proxyRedirectUrl; + } + } if (requestParameters.serviceConfigurationParameters != nil) { OIDServiceConfiguration *serviceConfiguration = [self processServiceConfigurationParameters: @@ -206,6 +219,7 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl + proxyRedirectUrl:requestParameters.proxyRedirectUrl additionalParameters:requestParameters.additionalParameters externalUserAgent:requestParameters.externalUserAgent result:result @@ -242,6 +256,9 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments redirectUrl: requestParameters .redirectUrl + proxyRedirectUrl: + requestParameters + .proxyRedirectUrl additionalParameters: requestParameters .additionalParameters @@ -282,6 +299,9 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments redirectUrl: requestParameters .redirectUrl + proxyRedirectUrl: + requestParameters + .proxyRedirectUrl additionalParameters: requestParameters .additionalParameters @@ -448,11 +468,12 @@ - (void)handleEndSessionMethodCall:(NSDictionary *)arguments - (void)performTokenRequest:(OIDServiceConfiguration *)serviceConfiguration requestParameters:(TokenRequestParameters *)requestParameters result:(FlutterResult)result { + NSString *effectiveRedirectUrl = requestParameters.proxyRedirectUrl ?: requestParameters.redirectUrl; OIDTokenRequest *tokenRequest = [[OIDTokenRequest alloc] initWithConfiguration:serviceConfiguration grantType:requestParameters.grantType authorizationCode:requestParameters.authorizationCode - redirectURL:[NSURL URLWithString:requestParameters.redirectUrl] + redirectURL:[NSURL URLWithString:effectiveRedirectUrl] clientID:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes @@ -505,6 +526,15 @@ - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; NSURL *URL = [NSURL URLWithString:URLString]; + if (_pendingProxyRedirectUrl) { + NSURLComponents *proxyComponents = + [NSURLComponents componentsWithString:_pendingProxyRedirectUrl]; + NSURLComponents *incomingComponents = + [NSURLComponents componentsWithURL:URL resolvingAgainstBaseURL:NO]; + proxyComponents.queryItems = incomingComponents.queryItems; + URL = [proxyComponents URL]; + _pendingProxyRedirectUrl = nil; + } [_currentAuthorizationFlow resumeExternalUserAgentFlowWithURL:URL]; _currentAuthorizationFlow = nil; } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_request.dart index a0a2fd3a..6e39b9d2 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_request.dart @@ -9,6 +9,7 @@ class AuthorizationRequest extends CommonRequestDetails AuthorizationRequest( String clientId, String redirectUrl, { + String? proxyRedirectUrl, String? issuer, String? discoveryUrl, AuthorizationServiceConfiguration? serviceConfiguration, @@ -24,6 +25,7 @@ class AuthorizationRequest extends CommonRequestDetails }) { this.clientId = clientId; this.redirectUrl = redirectUrl; + this.proxyRedirectUrl = proxyRedirectUrl; this.scopes = scopes; this.serviceConfiguration = serviceConfiguration; this.additionalParameters = additionalParameters; 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 8436c45d..c2699ec2 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart @@ -9,6 +9,7 @@ class AuthorizationTokenRequest extends TokenRequest AuthorizationTokenRequest( super.clientId, super.redirectUrl, { + super.proxyRedirectUrl, String? loginHint, super.clientSecret, super.scopes, diff --git a/flutter_appauth_platform_interface/lib/src/common_request_details.dart b/flutter_appauth_platform_interface/lib/src/common_request_details.dart index 6ac75b47..664daa1b 100644 --- a/flutter_appauth_platform_interface/lib/src/common_request_details.dart +++ b/flutter_appauth_platform_interface/lib/src/common_request_details.dart @@ -8,6 +8,15 @@ class CommonRequestDetails /// The redirect URL. late String redirectUrl; + /// An optional HTTPS proxy redirect URL. + /// + /// Some OAuth servers only allow HTTPS redirect URIs. When set, this URL is + /// sent to the OAuth server as the `redirect_uri` parameter, while + /// [redirectUrl] (the custom-scheme deep link) is used to intercept the + /// callback. Token exchange also uses [proxyRedirectUrl] so that it matches + /// what was sent in the authorization request. + String? proxyRedirectUrl; + /// The request scopes. List? scopes; 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 aab82ea2..9b90fb52 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart @@ -15,6 +15,7 @@ Map _convertCommonRequestDetailsToMap( 'nonce': commonRequestDetails.nonce, 'discoveryUrl': commonRequestDetails.discoveryUrl, 'redirectUrl': commonRequestDetails.redirectUrl, + 'proxyRedirectUrl': commonRequestDetails.proxyRedirectUrl, 'scopes': commonRequestDetails.scopes, 'serviceConfiguration': commonRequestDetails.serviceConfiguration?.toMap(), 'additionalParameters': commonRequestDetails.additionalParameters, diff --git a/flutter_appauth_platform_interface/lib/src/token_request.dart b/flutter_appauth_platform_interface/lib/src/token_request.dart index f93b0244..d38f350b 100644 --- a/flutter_appauth_platform_interface/lib/src/token_request.dart +++ b/flutter_appauth_platform_interface/lib/src/token_request.dart @@ -6,6 +6,7 @@ class TokenRequest extends CommonRequestDetails { TokenRequest( String clientId, String redirectUrl, { + String? proxyRedirectUrl, this.clientSecret, List? scopes, String? issuer, @@ -21,6 +22,7 @@ class TokenRequest extends CommonRequestDetails { }) { this.clientId = clientId; this.redirectUrl = redirectUrl; + this.proxyRedirectUrl = proxyRedirectUrl; this.scopes = scopes; this.serviceConfiguration = serviceConfiguration; this.additionalParameters = additionalParameters; 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 dd59971e..b3d3a24f 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 @@ -32,14 +32,14 @@ void main() { 'clientId': 'someClientId', 'issuer': null, 'redirectUrl': 'someRedirectUrl', + 'proxyRedirectUrl': null, 'discoveryUrl': 'someDiscoveryUrl', 'loginHint': 'someLoginHint', 'scopes': null, 'serviceConfiguration': null, 'additionalParameters': null, 'allowInsecureConnections': false, - 'externalUserAgent': - ExternalUserAgent.asWebAuthenticationSession.index, + 'externalUserAgent': ExternalUserAgent.asWebAuthenticationSession.index, 'promptValues': null, 'responseMode': null, 'nonce': null, @@ -61,6 +61,7 @@ void main() { 'clientId': 'someClientId', 'issuer': null, 'redirectUrl': 'someRedirectUrl', + 'proxyRedirectUrl': null, 'discoveryUrl': 'someDiscoveryUrl', 'loginHint': 'someLoginHint', 'scopes': null, @@ -100,6 +101,7 @@ void main() { 'clientId': 'someClientId', 'issuer': null, 'redirectUrl': 'someRedirectUrl', + 'proxyRedirectUrl': null, 'discoveryUrl': 'someDiscoveryUrl', 'scopes': null, 'serviceConfiguration': null, @@ -127,6 +129,7 @@ void main() { 'clientId': 'someClientId', 'issuer': null, 'redirectUrl': 'someRedirectUrl', + 'proxyRedirectUrl': null, 'discoveryUrl': 'someDiscoveryUrl', 'scopes': null, 'serviceConfiguration': null, @@ -143,6 +146,35 @@ void main() { ); }); + test('passes proxyRedirectUrl', () async { + await flutterAppAuth.token(TokenRequest('someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + refreshToken: 'someRefreshToken', + proxyRedirectUrl: 'someProxyRedirectUrl')); + expect( + log, + [ + isMethodCall('token', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'proxyRedirectUrl': 'someProxyRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'clientSecret': null, + 'refreshToken': 'someRefreshToken', + 'authorizationCode': null, + 'grantType': 'refresh_token', + 'codeVerifier': null, + 'nonce': null, + }) + ], + ); + }); + test('sends specified grant type', () async { await flutterAppAuth.token(TokenRequest('someClientId', 'someRedirectUrl', discoveryUrl: 'someDiscoveryUrl', grantType: 'someGrantType')); @@ -153,6 +185,7 @@ void main() { 'clientId': 'someClientId', 'issuer': null, 'redirectUrl': 'someRedirectUrl', + 'proxyRedirectUrl': null, 'discoveryUrl': 'someDiscoveryUrl', 'scopes': null, 'serviceConfiguration': null,