From f8bd32e0df08a8f1211c1b0171d78e87114a9a77 Mon Sep 17 00:00:00 2001 From: Michael Bui <25263378+MaikuB@users.noreply.github.com> Date: Wed, 17 Nov 2021 20:30:08 +1100 Subject: [PATCH] [flutter_appauth][flutter_appauth_platform_interface] added support for end session requests (#216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * progress on end session on iOS * various fixes * further updates to platform interface to handle end session requests * bump AppAuth iOS dependency to 1.4.0 * update AuthorizationServiceConfiguration to have nullable props * complete wiring up end session request on Android and iOS * update plugin for prerelease * add missing changelog entry on SDK dependencies being bumped * update Dart SDK constraints * update readmes and add mention to changelog * fix null endSessionEndpoint crash on iOS * bump platform interface * fix merge issue * bump plugin * Skip https issuer check if allowInsecureConnections is true. (#228) * bump to 2.0.0-dev.3 * bump AppAuth Android dependency * create external user agent per request, which also issue where cancelling an end session request once won't allow for subsequent sessions to work * fix example app so that refreshing access token works again * bump for release * fix imports in example app * fix spacing in changelog * bump platform interface to 4.0.0 stable release * bump plugin to 2.0.0 stable Co-authored-by: Roman Fürst --- README.md | 4 +- flutter_appauth/CHANGELOG.md | 8 + flutter_appauth/README.md | 25 ++- .../flutterappauth/FlutterAppauthPlugin.java | 184 +++++++++++++---- flutter_appauth/example/lib/main.dart | 64 ++++-- .../ios/Classes/FlutterAppauthPlugin.m | 190 +++++++++++++----- flutter_appauth/lib/flutter_appauth.dart | 2 + flutter_appauth/lib/src/flutter_appauth.dart | 4 + flutter_appauth/pubspec.yaml | 4 +- .../CHANGELOG.md | 5 + .../flutter_appauth_platform_interface.dart | 2 + ...ization_service_configuration_details.dart | 18 ++ .../lib/src/authorization_request.dart | 7 +- .../authorization_service_configuration.dart | 11 +- .../lib/src/common_request_details.dart | 14 +- .../lib/src/end_session_request.dart | 40 ++++ .../lib/src/end_session_response.dart | 5 + .../lib/src/flutter_appauth_platform.dart | 8 +- .../src/method_channel_flutter_appauth.dart | 12 ++ .../lib/src/method_channel_mappers.dart | 21 +- .../lib/src/token_request.dart | 14 +- .../pubspec.yaml | 2 +- .../method_channel_flutter_appauth_test.dart | 20 ++ 23 files changed, 518 insertions(+), 146 deletions(-) create mode 100644 flutter_appauth_platform_interface/lib/src/accepted_authorization_service_configuration_details.dart create mode 100644 flutter_appauth_platform_interface/lib/src/end_session_request.dart create mode 100644 flutter_appauth_platform_interface/lib/src/end_session_response.dart diff --git a/README.md b/README.md index 793eb79f..4981d498 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # flutter_appauth A Flutter plugin that provides a wrapper for native AppAuth SDKs (https://appauth.io) used authenticating and authorizing users. The repository consists of the following folders -- flutter_appauth: code for the plugin -- flutter_appauth_platform_interface: the code for common platform interface \ No newline at end of file +- [flutter_appauth](https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth): code for the plugin +- [flutter_appauth_platform_interface](https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth_platform_interface): the code for common platform interface \ No newline at end of file diff --git a/flutter_appauth/CHANGELOG.md b/flutter_appauth/CHANGELOG.md index 6844180b..946ffb6e 100644 --- a/flutter_appauth/CHANGELOG.md +++ b/flutter_appauth/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* **Breaking change** `AuthorizationServiceConfiguration` constructor has changed to take named parameters +* Added `endSession()` method, `EndSessionRequest` and `EndSessionResponse` classes to support end session requests +* [Android] skips https issuer check if `allowInsecureConnections` is true. Thanks to the PR from [Roman Fürst](https://github.com/rfuerst87) +* Bumped AppAuth Android and iOS SDK dependencies +* Added FAQs section to readme to describe a common iOS issue with Azure B2C and Azure AD + ## 1.1.1 * [Android] Migrate maven repository from jcenter to mavenCentral. diff --git a/flutter_appauth/README.md b/flutter_appauth/README.md index 47aaf9c0..78a1c481 100644 --- a/flutter_appauth/README.md +++ b/flutter_appauth/README.md @@ -54,15 +54,15 @@ final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode ); ``` -If you already know the authorization and token endpoints, which may be because discovery isn't supported, then these could be explicitly specified +In the event that discovery isn't supported or that you already know the endpoints for your server, they could be explicitly specified in the event that the dis ```dart final AuthorizationTokenResponse result = await appAuth.authorizeAndExchangeCode( AuthorizationTokenRequest( '', '', - serviceConfiguration: AuthorizationServiceConfiguration('', ''), - scopes: ['openid','profile', 'email', 'offline_access', 'api'] + serviceConfiguration: AuthorizationServiceConfiguration(authorizationEndpoint: '', tokenEndpooint: '', endSessionEndpoint: ''), + scopes: [...] ), ); ``` @@ -90,6 +90,19 @@ final TokenResponse result = await appAuth.token(TokenRequest('', '', + postLogoutRedirectUrl: '', + serviceConfiguration: AuthorizationServiceConfiguration(authorizationEndpoint: '', tokenEndpooint: '', endSessionEndpoint: '')); +``` + +The above code passes an `AuthorizationServiceConfiguration` with all the endpoints defined but alternatives are to specify an `issuer` or `discoveryUrl` like you would with the other APIs in the plugin (e.g. `authorizeAndExchangeCode()`). + ## Android setup Go to the `build.gradle` 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 @@ -145,3 +158,9 @@ Go to the `Info.plist` for your iOS app to specify the custom scheme so that the ``` + +## FAQs + +**When connecting to Azure B2C or Azure AD, the login request redirects properly on Android but not on iOS. What's going on?** + +The AppAuth iOS SDK has some logic to validate the redirect URL to see if it should be responsible for processing the redirect. This appears to be failing under certain circumstances. Adding a trailing slash to the redirect URL specified in your code has been reported to fix the issue. 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 196129aa..e8fb7103 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 @@ -5,6 +5,9 @@ import android.content.Intent; import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import net.openid.appauth.AppAuthConfiguration; import net.openid.appauth.AuthorizationException; import net.openid.appauth.AuthorizationRequest; @@ -12,18 +15,17 @@ import net.openid.appauth.AuthorizationService; import net.openid.appauth.AuthorizationServiceConfiguration; import net.openid.appauth.ClientSecretBasic; +import net.openid.appauth.EndSessionRequest; +import net.openid.appauth.EndSessionResponse; import net.openid.appauth.ResponseTypeValues; import net.openid.appauth.TokenRequest; import net.openid.appauth.TokenResponse; - import net.openid.appauth.connectivity.DefaultConnectionBuilder; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; -import androidx.annotation.Nullable; - import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -41,20 +43,26 @@ public class FlutterAppauthPlugin implements FlutterPlugin, MethodCallHandler, P private static final String AUTHORIZE_AND_EXCHANGE_CODE_METHOD = "authorizeAndExchangeCode"; private static final String AUTHORIZE_METHOD = "authorize"; private static final String TOKEN_METHOD = "token"; + private static final String END_SESSION_METHOD = "endSession"; private static final String DISCOVERY_ERROR_CODE = "discovery_failed"; private static final String AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE = "authorize_and_exchange_code_failed"; private static final String AUTHORIZE_ERROR_CODE = "authorize_failed"; private static final String TOKEN_ERROR_CODE = "token_failed"; + private static final String END_SESSION_ERROR_CODE = "end_session_failed"; private static final String NULL_INTENT_ERROR_CODE = "null_intent"; private static final String DISCOVERY_ERROR_MESSAGE_FORMAT = "Error retrieving discovery document: [error: %s, description: %s]"; private static final String TOKEN_ERROR_MESSAGE_FORMAT = "Failed to get token: [error: %s, description: %s]"; private static final String AUTHORIZE_ERROR_MESSAGE_FORMAT = "Failed to authorize: [error: %s, description: %s]"; + private static final String END_SESSION_ERROR_MESSAGE_FORMAT = "Failed to end session: [error: %s, description: %s]"; + private static final String NULL_INTENT_ERROR_FORMAT = "Failed to authorize: Null intent received"; private final int RC_AUTH_EXCHANGE_CODE = 65030; private final int RC_AUTH = 65031; + private final int RC_END_SESSION = 65032; + private Context applicationContext; private Activity mainActivity; private PendingOperation pendingOperation; @@ -100,7 +108,7 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { disposeAuthorizationServices(); } @@ -141,15 +149,16 @@ private void checkAndSetPendingOperation(String method, Result result) { pendingOperation = new PendingOperation(method, result); } + @Override - public void onMethodCall(MethodCall call, Result result) { + public void onMethodCall(MethodCall call, @NonNull Result result) { Map arguments = call.arguments(); switch (call.method) { case AUTHORIZE_AND_EXCHANGE_CODE_METHOD: try { checkAndSetPendingOperation(call.method, result); handleAuthorizeMethodCall(arguments, true); - } catch(Exception ex) { + } catch (Exception ex) { finishWithError(AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE, ex.getLocalizedMessage()); } break; @@ -157,7 +166,7 @@ public void onMethodCall(MethodCall call, Result result) { try { checkAndSetPendingOperation(call.method, result); handleAuthorizeMethodCall(arguments, false); - } catch(Exception ex) { + } catch (Exception ex) { finishWithError(AUTHORIZE_ERROR_CODE, ex.getLocalizedMessage()); } break; @@ -165,10 +174,18 @@ public void onMethodCall(MethodCall call, Result result) { try { checkAndSetPendingOperation(call.method, result); handleTokenMethodCall(arguments); - } catch(Exception ex) { + } catch (Exception ex) { finishWithError(TOKEN_ERROR_CODE, ex.getLocalizedMessage()); } break; + case END_SESSION_METHOD: + try { + checkAndSetPendingOperation(call.method, result); + handleEndSessionMethodCall(arguments); + } catch (Exception ex) { + finishWithError(END_SESSION_ERROR_CODE, ex.getLocalizedMessage()); + } + break; default: result.notImplemented(); } @@ -213,16 +230,29 @@ private TokenRequestParameters processTokenRequestArguments(Map codeVerifier = (String) arguments.get("codeVerifier"); } final ArrayList scopes = (ArrayList) arguments.get("scopes"); - Map serviceConfigurationParameters = (Map) arguments.get("serviceConfiguration"); - Map additionalParameters = (Map) arguments.get("additionalParameters"); + final Map serviceConfigurationParameters = (Map) arguments.get("serviceConfiguration"); + final Map additionalParameters = (Map) arguments.get("additionalParameters"); allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); return new TokenRequestParameters(clientId, issuer, discoveryUrl, scopes, redirectUrl, refreshToken, authorizationCode, codeVerifier, grantType, serviceConfigurationParameters, additionalParameters); } + @SuppressWarnings("unchecked") + private EndSessionRequestParameters processEndSessionRequestArguments(Map arguments) { + final String idTokenHint = (String) arguments.get("idTokenHint"); + final String postLogoutRedirectUrl = (String) arguments.get("postLogoutRedirectUrl"); + final String state = (String) arguments.get("state"); + final boolean allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); + final String issuer = (String) arguments.get("issuer"); + final String discoveryUrl = (String) arguments.get("discoveryUrl"); + final Map serviceConfigurationParameters = (Map) arguments.get("serviceConfiguration"); + final Map additionalParameters = (Map) arguments.get("additionalParameters"); + return new EndSessionRequestParameters(idTokenHint, postLogoutRedirectUrl, state, issuer, discoveryUrl, allowInsecureConnections, serviceConfigurationParameters, additionalParameters); + } + private void handleAuthorizeMethodCall(Map arguments, final boolean exchangeCode) { final AuthorizationTokenRequestParameters tokenRequestParameters = processAuthorizationTokenRequestArguments(arguments); if (tokenRequestParameters.serviceConfigurationParameters != null) { - AuthorizationServiceConfiguration serviceConfiguration = requestParametersToServiceConfiguration(tokenRequestParameters); + AuthorizationServiceConfiguration serviceConfiguration = processServiceConfigurationParameters(tokenRequestParameters.serviceConfigurationParameters); performAuthorization(serviceConfiguration, tokenRequestParameters.clientId, tokenRequestParameters.redirectUrl, tokenRequestParameters.scopes, tokenRequestParameters.loginHint, tokenRequestParameters.additionalParameters, exchangeCode, tokenRequestParameters.promptValues, tokenRequestParameters.responseMode); } else { AuthorizationServiceConfiguration.RetrieveConfigurationCallback callback = new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { @@ -238,47 +268,36 @@ public void onFetchConfigurationCompleted(@Nullable AuthorizationServiceConfigur if (tokenRequestParameters.discoveryUrl != null) { AuthorizationServiceConfiguration.fetchFromUrl(Uri.parse(tokenRequestParameters.discoveryUrl), callback, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); } else { - AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse(tokenRequestParameters.issuer), callback); + AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse(tokenRequestParameters.issuer), callback, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); } } - } - private AuthorizationServiceConfiguration requestParametersToServiceConfiguration(TokenRequestParameters tokenRequestParameters) { - return new AuthorizationServiceConfiguration(Uri.parse(tokenRequestParameters.serviceConfigurationParameters.get("authorizationEndpoint")), Uri.parse(tokenRequestParameters.serviceConfigurationParameters.get("tokenEndpoint"))); + private AuthorizationServiceConfiguration processServiceConfigurationParameters(Map serviceConfigurationArguments) { + final String endSessionEndpoint = serviceConfigurationArguments.get("endSessionEndpoint"); + return new AuthorizationServiceConfiguration(Uri.parse(serviceConfigurationArguments.get("authorizationEndpoint")), Uri.parse(serviceConfigurationArguments.get("tokenEndpoint")), null, endSessionEndpoint == null ? null : Uri.parse(endSessionEndpoint)); } private void handleTokenMethodCall(Map arguments) { final TokenRequestParameters tokenRequestParameters = processTokenRequestArguments(arguments); if (tokenRequestParameters.serviceConfigurationParameters != null) { - - AuthorizationServiceConfiguration serviceConfiguration = requestParametersToServiceConfiguration(tokenRequestParameters); + AuthorizationServiceConfiguration serviceConfiguration = processServiceConfigurationParameters(tokenRequestParameters.serviceConfigurationParameters); performTokenRequest(serviceConfiguration, tokenRequestParameters); } else { - if (tokenRequestParameters.discoveryUrl != null) { - AuthorizationServiceConfiguration.fetchFromUrl(Uri.parse(tokenRequestParameters.discoveryUrl), new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { - @Override - public void onFetchConfigurationCompleted(@Nullable AuthorizationServiceConfiguration serviceConfiguration, @Nullable AuthorizationException ex) { - if (ex == null) { - performTokenRequest(serviceConfiguration, tokenRequestParameters); - } else { - finishWithDiscoveryError(ex); - } + AuthorizationServiceConfiguration.RetrieveConfigurationCallback callback = new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { + @Override + public void onFetchConfigurationCompleted(@Nullable AuthorizationServiceConfiguration serviceConfiguration, @Nullable AuthorizationException ex) { + if (ex == null) { + performTokenRequest(serviceConfiguration, tokenRequestParameters); + } else { + finishWithDiscoveryError(ex); } - }, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); - + } + }; + if (tokenRequestParameters.discoveryUrl != null) { + AuthorizationServiceConfiguration.fetchFromUrl(Uri.parse(tokenRequestParameters.discoveryUrl), callback, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); } else { - - AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse(tokenRequestParameters.issuer), new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { - @Override - public void onFetchConfigurationCompleted(@Nullable AuthorizationServiceConfiguration serviceConfiguration, @Nullable AuthorizationException ex) { - if (ex == null) { - performTokenRequest(serviceConfiguration, tokenRequestParameters); - } else { - finishWithDiscoveryError(ex); - } - } - }); + AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse(tokenRequestParameters.issuer), callback, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); } } } @@ -355,7 +374,55 @@ public void onTokenRequestCompleted( } else { authorizationService.performTokenRequest(tokenRequest, new ClientSecretBasic(clientSecret), tokenResponseCallback); } + } + + private void handleEndSessionMethodCall(Map arguments) { + final EndSessionRequestParameters endSessionRequestParameters = processEndSessionRequestArguments(arguments); + if (endSessionRequestParameters.serviceConfigurationParameters != null) { + AuthorizationServiceConfiguration serviceConfiguration = processServiceConfigurationParameters(endSessionRequestParameters.serviceConfigurationParameters); + performEndSessionRequest(serviceConfiguration, endSessionRequestParameters); + } else { + AuthorizationServiceConfiguration.RetrieveConfigurationCallback callback = new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { + @Override + public void onFetchConfigurationCompleted(@Nullable AuthorizationServiceConfiguration serviceConfiguration, @Nullable AuthorizationException ex) { + if (ex == null) { + performEndSessionRequest(serviceConfiguration, endSessionRequestParameters); + } else { + finishWithDiscoveryError(ex); + } + } + }; + + if (endSessionRequestParameters.discoveryUrl != null) { + AuthorizationServiceConfiguration.fetchFromUrl(Uri.parse(endSessionRequestParameters.discoveryUrl), callback, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); + } else { + AuthorizationServiceConfiguration.fetchFromIssuer(Uri.parse(endSessionRequestParameters.issuer), callback, allowInsecureConnections ? InsecureConnectionBuilder.INSTANCE : DefaultConnectionBuilder.INSTANCE); + } + } + } + + private void performEndSessionRequest(AuthorizationServiceConfiguration serviceConfiguration, final EndSessionRequestParameters endSessionRequestParameters) { + EndSessionRequest.Builder endSessionRequestBuilder = new EndSessionRequest.Builder(serviceConfiguration); + if (endSessionRequestParameters.idTokenHint != null) { + endSessionRequestBuilder.setIdTokenHint(endSessionRequestParameters.idTokenHint); + } + + if (endSessionRequestParameters.postLogoutRedirectUrl != null) { + endSessionRequestBuilder.setPostLogoutRedirectUri(Uri.parse(endSessionRequestParameters.postLogoutRedirectUrl)); + } + + if (endSessionRequestParameters.state != null) { + endSessionRequestBuilder.setState(endSessionRequestParameters.state); + } + if (endSessionRequestParameters.additionalParameters != null) { + endSessionRequestBuilder.setAdditionalParameters(endSessionRequestParameters.additionalParameters); + } + + final EndSessionRequest endSessionRequest = endSessionRequestBuilder.build(); + AuthorizationService authorizationService = allowInsecureConnections ? insecureAuthorizationService : defaultAuthorizationService; + Intent endSessionIntent = authorizationService.getEndSessionRequestIntent(endSessionRequest); + mainActivity.startActivityForResult(endSessionIntent, RC_END_SESSION); } private void finishWithTokenError(AuthorizationException ex) { @@ -381,6 +448,10 @@ private void finishWithDiscoveryError(AuthorizationException ex) { finishWithError(DISCOVERY_ERROR_CODE, String.format(DISCOVERY_ERROR_MESSAGE_FORMAT, ex.error, ex.errorDescription)); } + private void finishWithEndSessionError(AuthorizationException ex) { + finishWithError(END_SESSION_ERROR_CODE, String.format(END_SESSION_ERROR_MESSAGE_FORMAT, ex.error, ex.errorDescription)); + } + @Override public boolean onActivityResult(int requestCode, int resultCode, Intent intent) { @@ -397,6 +468,17 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent intent) } return true; } + if (requestCode == RC_END_SESSION) { + final EndSessionResponse endSessionResponse = EndSessionResponse.fromIntent(intent); + AuthorizationException ex = AuthorizationException.fromIntent(intent); + if (ex != null) { + finishWithEndSessionError(ex); + } else { + Map responseMap = new HashMap<>(); + responseMap.put("state", endSessionResponse.state); + finishWithSuccess(responseMap); + } + } return false; } @@ -406,6 +488,7 @@ private void processAuthorizationData(final AuthorizationResponse authResponse, AppAuthConfiguration.Builder authConfigBuilder = new AppAuthConfiguration.Builder(); if (allowInsecureConnections) { authConfigBuilder.setConnectionBuilder(InsecureConnectionBuilder.INSTANCE); + authConfigBuilder.setSkipIssuerHttpsCheck(true); } AuthorizationService authService = new AuthorizationService(applicationContext, authConfigBuilder.build()); @@ -466,7 +549,6 @@ private class PendingOperation { } } - private class TokenRequestParameters { final String clientId; final String issuer; @@ -495,6 +577,28 @@ private TokenRequestParameters(String clientId, String issuer, String discoveryU } } + private class EndSessionRequestParameters { + final String idTokenHint; + final String postLogoutRedirectUrl; + final String state; + final String issuer; + final String discoveryUrl; + final boolean allowInsecureConnections; + final Map serviceConfigurationParameters; + final Map additionalParameters; + + private EndSessionRequestParameters(String idTokenHint, String postLogoutRedirectUrl, String state, String issuer, String discoveryUrl, boolean allowInsecureConnections, Map serviceConfigurationParameters, Map additionalParameters) { + this.idTokenHint = idTokenHint; + this.postLogoutRedirectUrl = postLogoutRedirectUrl; + this.state = state; + this.issuer = issuer; + this.discoveryUrl = discoveryUrl; + this.allowInsecureConnections = allowInsecureConnections; + this.serviceConfigurationParameters = serviceConfigurationParameters; + this.additionalParameters = additionalParameters; + } + } + private class AuthorizationTokenRequestParameters extends TokenRequestParameters { final String loginHint; final ArrayList promptValues; diff --git a/flutter_appauth/example/lib/main.dart b/flutter_appauth/example/lib/main.dart index c80d9801..0505f939 100644 --- a/flutter_appauth/example/lib/main.dart +++ b/flutter_appauth/example/lib/main.dart @@ -1,7 +1,7 @@ import 'dart:io' show Platform; import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:http/http.dart' as http; void main() => runApp(MyApp()); @@ -17,6 +17,8 @@ class _MyAppState extends State { String? _authorizationCode; String? _refreshToken; String? _accessToken; + String? _idToken; + final TextEditingController _authorizationCodeTextController = TextEditingController(); final TextEditingController _accessTokenTextController = @@ -27,7 +29,7 @@ class _MyAppState extends State { final TextEditingController _idTokenTextController = TextEditingController(); final TextEditingController _refreshTokenTextController = TextEditingController(); - String _userInfo = ''; + String? _userInfo; // For a list of client IDs, go to https://demo.identityserver.io final String _clientId = 'interactive.public'; @@ -35,6 +37,7 @@ class _MyAppState extends State { final String _issuer = 'https://demo.identityserver.io'; final String _discoveryUrl = 'https://demo.identityserver.io/.well-known/openid-configuration'; + final String _postLogoutRedirectUrl = 'io.identityserver.demo:/'; final List _scopes = [ 'openid', 'profile', @@ -45,13 +48,10 @@ class _MyAppState extends State { final AuthorizationServiceConfiguration _serviceConfiguration = const AuthorizationServiceConfiguration( - 'https://demo.identityserver.io/connect/authorize', - 'https://demo.identityserver.io/connect/token'); - - @override - void initState() { - super.initState(); - } + authorizationEndpoint: 'https://demo.identityserver.io/connect/authorize', + tokenEndpoint: 'https://demo.identityserver.io/connect/token', + endSessionEndpoint: 'https://demo.identityserver.io/connect/endsession', + ); @override Widget build(BuildContext context) { @@ -95,6 +95,14 @@ class _MyAppState extends State { child: const Text('Refresh token'), onPressed: _refreshToken != null ? _refresh : null, ), + ElevatedButton( + child: const Text('End session'), + onPressed: _idToken != null + ? () async { + await _endSession(); + } + : null, + ), const Text('authorization code'), TextField( controller: _authorizationCodeTextController, @@ -116,7 +124,7 @@ class _MyAppState extends State { controller: _refreshTokenTextController, ), const Text('test api results'), - Text(_userInfo), + Text(_userInfo ?? ''), ], ), ), @@ -124,14 +132,40 @@ class _MyAppState extends State { ); } + Future _endSession() async { + try { + _setBusyState(); + await _appAuth.endSession(EndSessionRequest( + idTokenHint: _idToken, + postLogoutRedirectUrl: _postLogoutRedirectUrl, + serviceConfiguration: _serviceConfiguration)); + _clearSessionInfo(); + } catch (_) {} + _clearBusyState(); + } + + void _clearSessionInfo() { + setState(() { + _codeVerifier = null; + _authorizationCode = null; + _authorizationCodeTextController.clear(); + _accessToken = null; + _accessTokenTextController.clear(); + _idToken = null; + _idTokenTextController.clear(); + _refreshToken = null; + _refreshTokenTextController.clear(); + _accessTokenExpirationTextController.clear(); + _userInfo = null; + }); + } + Future _refresh() async { try { _setBusyState(); final TokenResponse? result = await _appAuth.token(TokenRequest( _clientId, _redirectUrl, - refreshToken: _refreshToken, - discoveryUrl: _discoveryUrl, - scopes: _scopes)); + refreshToken: _refreshToken, issuer: _issuer, scopes: _scopes)); _processTokenResponse(result); await _testApi(result); } catch (_) { @@ -230,7 +264,7 @@ class _MyAppState extends State { void _processAuthTokenResponse(AuthorizationTokenResponse response) { setState(() { _accessToken = _accessTokenTextController.text = response.accessToken!; - _idTokenTextController.text = response.idToken!; + _idToken = _idTokenTextController.text = response.idToken!; _refreshToken = _refreshTokenTextController.text = response.refreshToken!; _accessTokenExpirationTextController.text = response.accessTokenExpirationDateTime!.toIso8601String(); @@ -250,7 +284,7 @@ class _MyAppState extends State { void _processTokenResponse(TokenResponse? response) { setState(() { _accessToken = _accessTokenTextController.text = response!.accessToken!; - _idTokenTextController.text = response.idToken!; + _idToken = _idTokenTextController.text = response.idToken!; _refreshToken = _refreshTokenTextController.text = response.refreshToken!; _accessTokenExpirationTextController.text = response.accessTokenExpirationDateTime!.toIso8601String(); diff --git a/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m b/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m index 02a9ee8a..79f5b272 100644 --- a/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m +++ b/flutter_appauth/ios/Classes/FlutterAppauthPlugin.m @@ -70,19 +70,46 @@ - (id)initWithArguments:(NSDictionary *)arguments { } @end +@interface EndSessionRequestParameters : NSObject +@property(nonatomic, strong) NSString *idTokenHint; +@property(nonatomic, strong) NSString *postLogoutRedirectUrl; +@property(nonatomic, strong) NSString *state; +@property(nonatomic, strong) NSString *issuer; +@property(nonatomic, strong) NSString *discoveryUrl; +@property(nonatomic, strong) NSDictionary *serviceConfigurationParameters; +@property(nonatomic, strong) NSDictionary *additionalParameters; +@end + +@implementation EndSessionRequestParameters +- (id)initWithArguments:(NSDictionary *)arguments { + _idTokenHint= [ArgumentProcessor processArgumentValue:arguments withKey:@"idTokenHint"]; + _postLogoutRedirectUrl = [ArgumentProcessor processArgumentValue:arguments withKey:@"postLogoutRedirectUrl"]; + _state = [ArgumentProcessor processArgumentValue:arguments withKey:@"state"]; + _issuer = [ArgumentProcessor processArgumentValue:arguments withKey:@"issuer"]; + _discoveryUrl = [ArgumentProcessor processArgumentValue:arguments withKey:@"discoveryUrl"]; + _serviceConfigurationParameters = [ArgumentProcessor processArgumentValue:arguments withKey:@"serviceConfiguration"]; + _additionalParameters = [ArgumentProcessor processArgumentValue:arguments withKey:@"additionalParameters"]; + return self; +} +@end + @implementation FlutterAppauthPlugin FlutterMethodChannel* channel; NSString *const AUTHORIZE_METHOD = @"authorize"; NSString *const AUTHORIZE_AND_EXCHANGE_CODE_METHOD = @"authorizeAndExchangeCode"; NSString *const TOKEN_METHOD = @"token"; +NSString *const END_SESSION_METHOD = @"endSession"; NSString *const AUTHORIZE_ERROR_CODE = @"authorize_failed"; NSString *const AUTHORIZE_AND_EXCHANGE_CODE_ERROR_CODE = @"authorize_and_exchange_code_failed"; NSString *const DISCOVERY_ERROR_CODE = @"discovery_failed"; NSString *const TOKEN_ERROR_CODE = @"token_failed"; +NSString *const END_SESSION_ERROR_CODE = @"end_session_failed"; NSString *const DISCOVERY_ERROR_MESSAGE_FORMAT = @"Error retrieving discovery document: %@"; NSString *const TOKEN_ERROR_MESSAGE_FORMAT = @"Failed to get token: %@"; NSString *const AUTHORIZE_ERROR_MESSAGE_FORMAT = @"Failed to authorize: %@"; +NSString *const END_SESSION_ERROR_MESSAGE_FORMAT = @"Failed to end session: %@"; + + (void)registerWithRegistrar:(NSObject*)registrar { @@ -101,6 +128,8 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self handleAuthorizeMethodCall:[call arguments] result:result exchangeCode:false]; } else if([TOKEN_METHOD isEqualToString:call.method]) { [self handleTokenMethodCall:[call arguments] result:result]; + } else if([END_SESSION_METHOD isEqualToString:call.method]) { + [self handleEndSessionMethodCall:[call arguments] result:result]; } else { result(FlutterMethodNotImplemented); } @@ -127,40 +156,35 @@ -(void)handleAuthorizeMethodCall:(NSDictionary*)arguments result:(FlutterResult) [requestParameters.additionalParameters setValue:requestParameters.responseMode forKey:@"response_mode"]; } if(requestParameters.serviceConfigurationParameters != nil) { - OIDServiceConfiguration *serviceConfiguration = - [[OIDServiceConfiguration alloc] - initWithAuthorizationEndpoint:[NSURL URLWithString:requestParameters.serviceConfigurationParameters[@"authorizationEndpoint"]] - tokenEndpoint:[NSURL URLWithString:requestParameters.serviceConfigurationParameters[@"tokenEndpoint"]]]; + OIDServiceConfiguration *serviceConfiguration = [self processServiceConfigurationParameters:requestParameters.serviceConfigurationParameters]; [self performAuthorization:serviceConfiguration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode]; } else if (requestParameters.discoveryUrl) { NSURL *discoveryUrl = [NSURL URLWithString:requestParameters.discoveryUrl]; [OIDAuthorizationService discoverServiceConfigurationForDiscoveryURL:discoveryUrl completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { - - if (!configuration) { - [self finishWithDiscoveryError:error result:result]; - return; - } - - [self performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode]; - }]; + + if (!configuration) { + [self finishWithDiscoveryError:error result:result]; + return; + } + + [self performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode]; + }]; } else { NSURL *issuerUrl = [NSURL URLWithString:requestParameters.issuer]; [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuerUrl completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { - - if (!configuration) { - [self finishWithDiscoveryError:error result:result]; - return; - } - + + if (!configuration) { + [self finishWithDiscoveryError:error result:result]; + return; + } + [self performAuthorization:configuration clientId:requestParameters.clientId clientSecret:requestParameters.clientSecret scopes:requestParameters.scopes redirectUrl:requestParameters.redirectUrl additionalParameters:requestParameters.additionalParameters preferEphemeralSession:requestParameters.preferEphemeralSession result:result exchangeCode:exchangeCode]; - }]; + }]; } - - } - (void)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{ @@ -175,10 +199,9 @@ - (void)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration cli UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController; if(exchangeCode) { - NSObject *agent = [self userAgentWithViewController:rootViewController useEphemeralSession:preferEphemeralSession]; - - _currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:agent callback:^(OIDAuthState *_Nullable authState, - NSError *_Nullable error) { + id externalUserAgent = [self userAgentWithViewController:rootViewController useEphemeralSession:preferEphemeralSession]; + _currentAuthorizationFlow = [OIDAuthState authStateByPresentingAuthorizationRequest:request externalUserAgent:externalUserAgent callback:^(OIDAuthState *_Nullable authState, + NSError *_Nullable error) { if(authState) { result([self processResponses:authState.lastTokenResponse authResponse:authState.lastAuthorizationResponse]); @@ -187,8 +210,8 @@ - (void)performAuthorization:(OIDServiceConfiguration *)serviceConfiguration cli } }]; } else { - NSObject *agent = [self userAgentWithViewController:rootViewController useEphemeralSession:preferEphemeralSession]; - _currentAuthorizationFlow = [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:agent callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) { + id externalUserAgent = [self userAgentWithViewController:rootViewController useEphemeralSession:preferEphemeralSession]; + _currentAuthorizationFlow = [OIDAuthorizationService presentAuthorizationRequest:request externalUserAgent:externalUserAgent callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) { if(authorizationResponse) { NSMutableDictionary *processedResponse = [[NSMutableDictionary alloc] init]; [processedResponse setObject:authorizationResponse.additionalParameters forKey:@"authorizationAdditionalParameters"]; @@ -226,13 +249,19 @@ - (void)finishWithError:(NSString *)errorCode message:(NSString *)message resul } +- (OIDServiceConfiguration *)processServiceConfigurationParameters:(NSDictionary*)serviceConfigurationParameters { + NSURL *endSessionEndpoint = serviceConfigurationParameters[@"endSessionEndpoint"] == [NSNull null] ? nil : [NSURL URLWithString:serviceConfigurationParameters[@"endSessionEndpoint"]]; + OIDServiceConfiguration *serviceConfiguration = + [[OIDServiceConfiguration alloc] + initWithAuthorizationEndpoint:[NSURL URLWithString:serviceConfigurationParameters[@"authorizationEndpoint"]] + tokenEndpoint:[NSURL URLWithString:serviceConfigurationParameters[@"tokenEndpoint"]] issuer:nil registrationEndpoint:nil endSessionEndpoint:endSessionEndpoint]; + return serviceConfiguration; +} + -(void)handleTokenMethodCall:(NSDictionary*)arguments result:(FlutterResult)result { TokenRequestParameters *requestParameters = [[TokenRequestParameters alloc] initWithArguments:arguments]; if(requestParameters.serviceConfigurationParameters != nil) { - OIDServiceConfiguration *serviceConfiguration = - [[OIDServiceConfiguration alloc] - initWithAuthorizationEndpoint:[NSURL URLWithString:requestParameters.serviceConfigurationParameters[@"authorizationEndpoint"]] - tokenEndpoint:[NSURL URLWithString:requestParameters.serviceConfigurationParameters[@"tokenEndpoint"]]]; + OIDServiceConfiguration *serviceConfiguration = [self processServiceConfigurationParameters:requestParameters.serviceConfigurationParameters]; [self performTokenRequest:serviceConfiguration requestParameters:requestParameters result:result]; } else if (requestParameters.discoveryUrl) { NSURL *discoveryUrl = [NSURL URLWithString:requestParameters.discoveryUrl]; @@ -240,29 +269,84 @@ -(void)handleTokenMethodCall:(NSDictionary*)arguments result:(FlutterResult)resu [OIDAuthorizationService discoverServiceConfigurationForDiscoveryURL:discoveryUrl completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { - - if (!configuration) { - [self finishWithDiscoveryError:error result:result]; - return; - } - - [self performTokenRequest:configuration requestParameters:requestParameters result:result]; - }]; + if (!configuration) { + [self finishWithDiscoveryError:error result:result]; + return; + } + + [self performTokenRequest:configuration requestParameters:requestParameters result:result]; + }]; } else { NSURL *issuerUrl = [NSURL URLWithString:requestParameters.issuer]; [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuerUrl completion:^(OIDServiceConfiguration *_Nullable configuration, NSError *_Nullable error) { - - if (!configuration) { - [self finishWithDiscoveryError:error result:result]; - return; - } - - [self performTokenRequest:configuration requestParameters:requestParameters result:result]; - }]; + if (!configuration) { + [self finishWithDiscoveryError:error result:result]; + return; + } + + [self performTokenRequest:configuration requestParameters:requestParameters result:result]; + }]; + } +} + +-(void)handleEndSessionMethodCall:(NSDictionary*)arguments result:(FlutterResult)result { + EndSessionRequestParameters *requestParameters = [[EndSessionRequestParameters alloc] initWithArguments:arguments]; + if(requestParameters.serviceConfigurationParameters != nil) { + OIDServiceConfiguration *serviceConfiguration = [self processServiceConfigurationParameters:requestParameters.serviceConfigurationParameters]; + [self performEndSessionRequest:serviceConfiguration requestParameters:requestParameters result:result]; + } else if (requestParameters.discoveryUrl) { + NSURL *discoveryUrl = [NSURL URLWithString:requestParameters.discoveryUrl]; + + [OIDAuthorizationService discoverServiceConfigurationForDiscoveryURL:discoveryUrl + completion:^(OIDServiceConfiguration *_Nullable configuration, + NSError *_Nullable error) { + if (!configuration) { + [self finishWithDiscoveryError:error result:result]; + return; + } + + [self performEndSessionRequest:configuration requestParameters:requestParameters result:result]; + }]; + } else { + NSURL *issuerUrl = [NSURL URLWithString:requestParameters.issuer]; + [OIDAuthorizationService discoverServiceConfigurationForIssuer:issuerUrl + completion:^(OIDServiceConfiguration *_Nullable configuration, + NSError *_Nullable error) { + if (!configuration) { + [self finishWithDiscoveryError:error result:result]; + return; + } + + [self performEndSessionRequest:configuration requestParameters:requestParameters result:result]; + }]; } +} + +- (void)performEndSessionRequest:(OIDServiceConfiguration *)serviceConfiguration requestParameters:(EndSessionRequestParameters *)requestParameters result:(FlutterResult)result { + NSURL *postLogoutRedirectURL = requestParameters.postLogoutRedirectUrl ? [NSURL URLWithString:requestParameters.postLogoutRedirectUrl] : nil; + OIDEndSessionRequest *endSessionRequest = requestParameters.state ? [[OIDEndSessionRequest alloc] initWithConfiguration:serviceConfiguration idTokenHint:requestParameters.idTokenHint postLogoutRedirectURL:postLogoutRedirectURL + state:requestParameters.state additionalParameters:requestParameters.additionalParameters] :[[OIDEndSessionRequest alloc] initWithConfiguration:serviceConfiguration idTokenHint:requestParameters.idTokenHint postLogoutRedirectURL:postLogoutRedirectURL + additionalParameters:requestParameters.additionalParameters]; + + UIViewController *rootViewController = + [UIApplication sharedApplication].delegate.window.rootViewController; + id externalUserAgent = [self userAgentWithViewController:rootViewController useEphemeralSession:false]; + + + _currentAuthorizationFlow = [OIDAuthorizationService presentEndSessionRequest:endSessionRequest externalUserAgent:externalUserAgent callback:^(OIDEndSessionResponse * _Nullable endSessionResponse, NSError * _Nullable error) { + self->_currentAuthorizationFlow = nil; + if(!endSessionResponse) { + NSString *message = [NSString stringWithFormat:END_SESSION_ERROR_MESSAGE_FORMAT, [error localizedDescription]]; + [self finishWithError:END_SESSION_ERROR_CODE message:message result:result]; + return; + } + NSMutableDictionary *processedResponse = [[NSMutableDictionary alloc] init]; + [processedResponse setObject:endSessionResponse.state forKey:@"state"]; + result(processedResponse); + }]; } - (void)performTokenRequest:(OIDServiceConfiguration *)serviceConfiguration requestParameters:(TokenRequestParameters *)requestParameters result:(FlutterResult)result { @@ -280,12 +364,12 @@ - (void)performTokenRequest:(OIDServiceConfiguration *)serviceConfiguration requ [OIDAuthorizationService performTokenRequest:tokenRequest callback:^(OIDTokenResponse *_Nullable response, NSError *_Nullable error) { - if (response) { - result([self processResponses:response authResponse:nil]); } else { - NSString *message = [NSString stringWithFormat:TOKEN_ERROR_MESSAGE_FORMAT, [error localizedDescription]]; - [self finishWithError:TOKEN_ERROR_CODE message:message result:result]; - } - }]; + if (response) { + result([self processResponses:response authResponse:nil]); } else { + NSString *message = [NSString stringWithFormat:TOKEN_ERROR_MESSAGE_FORMAT, [error localizedDescription]]; + [self finishWithError:TOKEN_ERROR_CODE message:message result:result]; + } + }]; } - (NSMutableDictionary *)processResponses:(OIDTokenResponse*) tokenResponse authResponse:(OIDAuthorizationResponse*) authResponse { diff --git a/flutter_appauth/lib/flutter_appauth.dart b/flutter_appauth/lib/flutter_appauth.dart index 5eb5f435..f3760b87 100644 --- a/flutter_appauth/lib/flutter_appauth.dart +++ b/flutter_appauth/lib/flutter_appauth.dart @@ -5,6 +5,8 @@ export 'package:flutter_appauth_platform_interface/flutter_appauth_platform_inte AuthorizationServiceConfiguration, AuthorizationTokenRequest, AuthorizationTokenResponse, + EndSessionRequest, + EndSessionResponse, GrantType, TokenRequest, TokenResponse; diff --git a/flutter_appauth/lib/src/flutter_appauth.dart b/flutter_appauth/lib/src/flutter_appauth.dart index 74a4f9fe..4f4bdc03 100644 --- a/flutter_appauth/lib/src/flutter_appauth.dart +++ b/flutter_appauth/lib/src/flutter_appauth.dart @@ -16,4 +16,8 @@ class FlutterAppAuth { Future token(TokenRequest request) { return FlutterAppAuthPlatform.instance.token(request); } + + Future endSession(EndSessionRequest request) { + return FlutterAppAuthPlatform.instance.endSession(request); + } } diff --git a/flutter_appauth/pubspec.yaml b/flutter_appauth/pubspec.yaml index 5f1cf016..aa4d9a1f 100644 --- a/flutter_appauth/pubspec.yaml +++ b/flutter_appauth/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_appauth description: This plugin provides an abstraction around the Android and iOS AppAuth SDKs so it can be used to communicate with OAuth 2.0 and OpenID Connect providers -version: 1.1.1 +version: 2.0.0 homepage: https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth environment: @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - flutter_appauth_platform_interface: ^3.0.0 + flutter_appauth_platform_interface: ^4.0.0 flutter: plugin: diff --git a/flutter_appauth_platform_interface/CHANGELOG.md b/flutter_appauth_platform_interface/CHANGELOG.md index f102f9fa..aad2f24d 100644 --- a/flutter_appauth_platform_interface/CHANGELOG.md +++ b/flutter_appauth_platform_interface/CHANGELOG.md @@ -1,3 +1,8 @@ +## [4.0.0] + +* **Breaking change** `AuthorizationServiceConfiguration` constructor has changed to take named parameters +* Added `endSession()` method, `EndSessionRequest` and `EndSessionResponse` classes to support end session requests + ## [3.1.0] * Added the ability to specify the response mode for authorization requests. This can be done using the `responseMode` parameter when constructing either an `AuthorizationRequest` or `AuthorizationTokenRequest`. This was done as the AppAuth Android SDK throws an exception when this was done via `additionalParameters` 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 3697e181..0afa637a 100644 --- a/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart +++ b/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart @@ -3,6 +3,8 @@ export 'src/authorization_response.dart'; export 'src/authorization_service_configuration.dart'; export 'src/authorization_token_request.dart'; export 'src/authorization_token_response.dart'; +export 'src/end_session_request.dart'; +export 'src/end_session_response.dart'; export 'src/grant_types.dart'; export 'src/token_request.dart'; export 'src/token_response.dart'; diff --git a/flutter_appauth_platform_interface/lib/src/accepted_authorization_service_configuration_details.dart b/flutter_appauth_platform_interface/lib/src/accepted_authorization_service_configuration_details.dart new file mode 100644 index 00000000..42bdc73a --- /dev/null +++ b/flutter_appauth_platform_interface/lib/src/accepted_authorization_service_configuration_details.dart @@ -0,0 +1,18 @@ +import 'authorization_service_configuration.dart'; + +mixin AcceptedAuthorizationServiceConfigurationDetails { + /// The issuer. + String? issuer; + + /// The URL of where the discovery document can be found. + String? discoveryUrl; + + /// The details of the OAuth 2.0 endpoints that can be explicitly provided when discovery isn't used or not possible. + AuthorizationServiceConfiguration? serviceConfiguration; + + void assertConfigurationInfo() { + assert( + issuer != null || discoveryUrl != null || serviceConfiguration != null, + 'Either the issuer, discovery URL or service configuration must be provided'); + } +} diff --git a/flutter_appauth_platform_interface/lib/src/authorization_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_request.dart index 294e2515..1efe1269 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_request.dart @@ -8,12 +8,12 @@ class AuthorizationRequest extends CommonRequestDetails AuthorizationRequest( String clientId, String redirectUrl, { + String? issuer, + String? discoveryUrl, + AuthorizationServiceConfiguration? serviceConfiguration, String? loginHint, List? scopes, - AuthorizationServiceConfiguration? serviceConfiguration, Map? additionalParameters, - String? issuer, - String? discoveryUrl, List? promptValues, bool allowInsecureConnections = false, bool preferEphemeralSession = false, @@ -31,5 +31,6 @@ class AuthorizationRequest extends CommonRequestDetails this.allowInsecureConnections = allowInsecureConnections; this.preferEphemeralSession = preferEphemeralSession; this.responseMode = responseMode; + assertConfigurationInfo(); } } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_service_configuration.dart b/flutter_appauth_platform_interface/lib/src/authorization_service_configuration.dart index f0f8c5e0..fb8b6dc4 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_service_configuration.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_service_configuration.dart @@ -1,10 +1,13 @@ class AuthorizationServiceConfiguration { - const AuthorizationServiceConfiguration( - this.authorizationEndpoint, - this.tokenEndpoint, - ); + const AuthorizationServiceConfiguration({ + required this.authorizationEndpoint, + required this.tokenEndpoint, + this.endSessionEndpoint, + }); final String authorizationEndpoint; final String tokenEndpoint; + + final String? endSessionEndpoint; } 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 5cc62d1e..169b1b13 100644 --- a/flutter_appauth_platform_interface/lib/src/common_request_details.dart +++ b/flutter_appauth_platform_interface/lib/src/common_request_details.dart @@ -1,24 +1,16 @@ -import 'authorization_service_configuration.dart'; +import 'accepted_authorization_service_configuration_details.dart'; -class CommonRequestDetails { +class CommonRequestDetails + with AcceptedAuthorizationServiceConfigurationDetails { /// The client id. late String clientId; - /// The issuer. - String? issuer; - - /// The URL of where the discovery document can be found. - String? discoveryUrl; - /// The redirect URL. late String redirectUrl; /// The request scopes. List? scopes; - /// The details of the OAuth 2.0 endpoints that can be explicitly when discovery isn't used or not possible. - AuthorizationServiceConfiguration? serviceConfiguration; - /// Additional parameters to include in the request. Map? additionalParameters; diff --git a/flutter_appauth_platform_interface/lib/src/end_session_request.dart b/flutter_appauth_platform_interface/lib/src/end_session_request.dart new file mode 100644 index 00000000..42ef89e0 --- /dev/null +++ b/flutter_appauth_platform_interface/lib/src/end_session_request.dart @@ -0,0 +1,40 @@ +import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart'; +import 'package:flutter_appauth_platform_interface/src/accepted_authorization_service_configuration_details.dart'; + +class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { + EndSessionRequest({ + this.idTokenHint, + this.postLogoutRedirectUrl, + this.state, + this.allowInsecureConnections = false, + this.additionalParameters, + String? issuer, + String? discoveryUrl, + AuthorizationServiceConfiguration? serviceConfiguration, + }) { + assert((idTokenHint == null && postLogoutRedirectUrl == null) || + (idTokenHint != null && postLogoutRedirectUrl != null)); + this.serviceConfiguration = serviceConfiguration; + this.issuer = issuer; + this.discoveryUrl = discoveryUrl; + } + + /// Represents the ID token previously issued to the user. + /// + /// Used to indicate the identity of the user requesting to be logged out. + final String? idTokenHint; + + /// Represents the URL to redirect to after the logout operation has been completed. + /// + /// When specified, the [idTokenHint] must also be provided. + final String? postLogoutRedirectUrl; + + final String? state; + + /// Whether to allow non-HTTPS endpoints. + /// + /// This property is only applicable to Android. + bool allowInsecureConnections; + + final Map? additionalParameters; +} diff --git a/flutter_appauth_platform_interface/lib/src/end_session_response.dart b/flutter_appauth_platform_interface/lib/src/end_session_response.dart new file mode 100644 index 00000000..56bff578 --- /dev/null +++ b/flutter_appauth_platform_interface/lib/src/end_session_response.dart @@ -0,0 +1,5 @@ +class EndSessionResponse { + final String? state; + + EndSessionResponse(this.state); +} diff --git a/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart b/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart index d6cf61b0..4eed3f8d 100644 --- a/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart +++ b/flutter_appauth_platform_interface/lib/src/flutter_appauth_platform.dart @@ -1,10 +1,12 @@ -import 'package:flutter_appauth_platform_interface/src/method_channel_flutter_appauth.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'authorization_request.dart'; import 'authorization_response.dart'; import 'authorization_token_request.dart'; import 'authorization_token_response.dart'; +import 'end_session_request.dart'; +import 'end_session_response.dart'; +import 'method_channel_flutter_appauth.dart'; import 'token_request.dart'; import 'token_response.dart'; @@ -44,4 +46,8 @@ abstract class FlutterAppAuthPlatform extends PlatformInterface { Future token(TokenRequest request) { throw UnimplementedError('token() has not been implemented'); } + + Future endSession(EndSessionRequest request) { + throw UnimplementedError('endSession() has not been implemented'); + } } diff --git a/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart b/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart index 00612c64..061f1a9b 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_flutter_appauth.dart @@ -1,4 +1,6 @@ import 'package:flutter/services.dart'; +import 'package:flutter_appauth_platform_interface/src/end_session_request.dart'; +import 'package:flutter_appauth_platform_interface/src/end_session_response.dart'; import 'authorization_request.dart'; import 'authorization_response.dart'; @@ -65,4 +67,14 @@ class MethodChannelFlutterAppAuth extends FlutterAppAuthPlatform { result['tokenType'], result['tokenAdditionalParameters']?.cast()); } + + @override + Future endSession(EndSessionRequest request) async { + final Map? result = + await _channel.invokeMethod('endSession', request.toMap()); + if (result == null) { + return null; + } + return EndSessionResponse(result['state']); + } } 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 6384b13b..7cb33fd6 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart @@ -3,6 +3,7 @@ import 'authorization_request.dart'; import 'authorization_service_configuration.dart'; import 'authorization_token_request.dart'; import 'common_request_details.dart'; +import 'end_session_request.dart'; import 'grant_types.dart'; import 'token_request.dart'; @@ -20,6 +21,21 @@ Map _convertCommonRequestDetailsToMap( }; } +extension EndSessionRequestMapper on EndSessionRequest { + Map toMap() { + return { + 'idTokenHint': idTokenHint, + 'postLogoutRedirectUrl': postLogoutRedirectUrl, + 'state': state, + 'allowInsecureConnections': allowInsecureConnections, + 'additionalParameters': additionalParameters, + 'issuer': issuer, + 'discoveryUrl': discoveryUrl, + 'serviceConfiguration': serviceConfiguration?.toMap(), + }; + } +} + extension AuthorizationRequestParameters on AuthorizationRequest { Map toMap() { return _convertAuthorizationParametersToMap(this) @@ -29,10 +45,11 @@ extension AuthorizationRequestParameters on AuthorizationRequest { extension AuthorizationServiceConfigurationMapper on AuthorizationServiceConfiguration { - Map toMap() { - return { + Map toMap() { + return { 'tokenEndpoint': tokenEndpoint, 'authorizationEndpoint': authorizationEndpoint, + 'endSessionEndpoint': endSessionEndpoint, }; } } diff --git a/flutter_appauth_platform_interface/lib/src/token_request.dart b/flutter_appauth_platform_interface/lib/src/token_request.dart index a2ae3130..4600d711 100644 --- a/flutter_appauth_platform_interface/lib/src/token_request.dart +++ b/flutter_appauth_platform_interface/lib/src/token_request.dart @@ -2,27 +2,22 @@ import 'authorization_service_configuration.dart'; import 'common_request_details.dart'; /// Details for a token exchange request. -class TokenRequest with CommonRequestDetails { +class TokenRequest extends CommonRequestDetails { TokenRequest( String clientId, String redirectUrl, { this.clientSecret, List? scopes, + String? issuer, + String? discoveryUrl, AuthorizationServiceConfiguration? serviceConfiguration, Map? additionalParameters, this.refreshToken, this.grantType, - String? issuer, - String? discoveryUrl, this.authorizationCode, this.codeVerifier, bool allowInsecureConnections = false, - }) : assert( - issuer != null || - discoveryUrl != null || - (serviceConfiguration?.authorizationEndpoint != null && - serviceConfiguration?.tokenEndpoint != null), - 'Either the issuer, discovery URL or service configuration must be provided') { + }) { this.clientId = clientId; this.redirectUrl = redirectUrl; this.scopes = scopes; @@ -31,6 +26,7 @@ class TokenRequest with CommonRequestDetails { this.issuer = issuer; this.discoveryUrl = discoveryUrl; this.allowInsecureConnections = allowInsecureConnections; + assertConfigurationInfo(); } /// The client secret. diff --git a/flutter_appauth_platform_interface/pubspec.yaml b/flutter_appauth_platform_interface/pubspec.yaml index 5b1aea51..7fdf5a85 100644 --- a/flutter_appauth_platform_interface/pubspec.yaml +++ b/flutter_appauth_platform_interface/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_appauth_platform_interface description: A common platform interface for the flutter_appauth plugin. -version: 3.1.0 +version: 4.0.0 homepage: https://github.com/MaikuB/flutter_appauth/tree/master/flutter_appauth_platform_interface environment: 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 8c3be216..26a4dc19 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 @@ -159,6 +159,26 @@ void main() { ); }); }); + + test('endSession', () async { + await flutterAppAuth.endSession(EndSessionRequest( + idTokenHint: 'someIdToken', + postLogoutRedirectUrl: 'somePostLogoutRedirectUrl', + state: 'someState', + discoveryUrl: 'someDiscoveryUrl')); + expect(log, [ + isMethodCall('endSession', arguments: { + 'idTokenHint': 'someIdToken', + 'postLogoutRedirectUrl': 'somePostLogoutRedirectUrl', + 'state': 'someState', + 'allowInsecureConnections': false, + 'additionalParameters': null, + 'issuer': null, + 'discoveryUrl': 'someDiscoveryUrl', + 'serviceConfiguration': null, + }) + ]); + }); } class FlutterAppAuthPlatformMock extends Mock