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..2e0e98f2 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 @@ -221,6 +221,9 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu (Map) arguments.get("additionalParameters"); allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); final String responseMode = (String) arguments.get("responseMode"); + final RequestState state = arguments.containsKey("state") + ? new RequestState((String) arguments.get("state")) + : null; return new AuthorizationTokenRequestParameters( clientId, @@ -233,7 +236,8 @@ private AuthorizationTokenRequestParameters processAuthorizationTokenRequestArgu loginHint, nonce, promptValues, - responseMode); + responseMode, + state); } @SuppressWarnings("unchecked") @@ -286,7 +290,9 @@ 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 RequestState state = arguments.containsKey("state") + ? new RequestState((String) arguments.get("state")) + : null; final boolean allowInsecureConnections = (boolean) arguments.get("allowInsecureConnections"); final String issuer = (String) arguments.get("issuer"); final String discoveryUrl = (String) arguments.get("discoveryUrl"); @@ -323,7 +329,8 @@ private void handleAuthorizeMethodCall( tokenRequestParameters.additionalParameters, exchangeCode, tokenRequestParameters.promptValues, - tokenRequestParameters.responseMode); + tokenRequestParameters.responseMode, + tokenRequestParameters.state); } else { AuthorizationServiceConfiguration.RetrieveConfigurationCallback callback = new AuthorizationServiceConfiguration.RetrieveConfigurationCallback() { @@ -342,7 +349,8 @@ public void onFetchConfigurationCompleted( tokenRequestParameters.additionalParameters, exchangeCode, tokenRequestParameters.promptValues, - tokenRequestParameters.responseMode); + tokenRequestParameters.responseMode, + tokenRequestParameters.state); } else { finishWithDiscoveryError(ex); } @@ -415,7 +423,8 @@ private void performAuthorization( Map additionalParameters, boolean exchangeCode, ArrayList promptValues, - String responseMode) { + String responseMode, + @Nullable RequestState state) { AuthorizationRequest.Builder authRequestBuilder = new AuthorizationRequest.Builder( serviceConfiguration, clientId, ResponseTypeValues.CODE, Uri.parse(redirectUrl)); @@ -461,6 +470,10 @@ private void performAuthorization( authRequestBuilder.setAdditionalParameters(additionalParameters); } + if (state != null) { + authRequestBuilder.setState(state.value); + } + AuthorizationService authorizationService = getAuthorizationService(); try { @@ -571,7 +584,7 @@ private void performEndSessionRequest( } if (endSessionRequestParameters.state != null) { - endSessionRequestBuilder.setState(endSessionRequestParameters.state); + endSessionRequestBuilder.setState(endSessionRequestParameters.state.value); } if (endSessionRequestParameters.additionalParameters != null) { @@ -827,10 +840,17 @@ private TokenRequestParameters( } } + /** Wraps an explicit state value from the method channel. + * null state = key absent (auto-generate); non-null = key was present (null value = suppress, String = custom). */ + private static class RequestState { + @Nullable final String value; + RequestState(@Nullable String value) { this.value = value; } + } + private class EndSessionRequestParameters { final String idTokenHint; final String postLogoutRedirectUrl; - final String state; + @Nullable final RequestState state; final String issuer; final String discoveryUrl; final boolean allowInsecureConnections; @@ -840,7 +860,7 @@ private class EndSessionRequestParameters { private EndSessionRequestParameters( String idTokenHint, String postLogoutRedirectUrl, - String state, + @Nullable RequestState state, String issuer, String discoveryUrl, boolean allowInsecureConnections, @@ -861,6 +881,7 @@ private class AuthorizationTokenRequestParameters extends TokenRequestParameters final String loginHint; final ArrayList promptValues; final String responseMode; + @Nullable final RequestState state; private AuthorizationTokenRequestParameters( String clientId, @@ -873,7 +894,8 @@ private AuthorizationTokenRequestParameters( String loginHint, String nonce, ArrayList promptValues, - String responseMode) { + String responseMode, + @Nullable RequestState state) { super( clientId, issuer, @@ -890,6 +912,7 @@ private AuthorizationTokenRequestParameters( this.loginHint = loginHint; this.promptValues = promptValues; this.responseMode = responseMode; + this.state = state; } } } 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..8ef173e2 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/AppAuthIOSAuthorization.m @@ -12,10 +12,14 @@ @implementation AppAuthIOSAuthorization externalUserAgent:(NSNumber *)externalUserAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode - nonce:(NSString *)nonce { + nonce:(NSString *)nonce + state:(id)state { NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier]; NSString *codeChallenge = [OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier]; + NSString *resolvedState = (state != nil) + ? ([state isKindOfClass:[NSString class]] ? state : nil) + : [OIDAuthorizationRequest generateState]; OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:serviceConfiguration @@ -24,7 +28,7 @@ @implementation AppAuthIOSAuthorization scope:[OIDScopeUtilities scopesWithArray:scopes] redirectURL:[NSURL URLWithString:redirectUrl] responseType:OIDResponseTypeCode - state:[OIDAuthorizationRequest generateState] + state:resolvedState nonce:nonce != nil ? nonce : [OIDAuthorizationRequest generateState] @@ -119,19 +123,18 @@ @implementation AppAuthIOSAuthorization ? [NSURL URLWithString:requestParameters.postLogoutRedirectUrl] : nil; + NSString *resolvedState = (requestParameters.state != nil) + ? ([requestParameters.state isKindOfClass:[NSString class]] + ? requestParameters.state + : nil) + : [OIDAuthorizationRequest generateState]; 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]; + [[OIDEndSessionRequest alloc] + initWithConfiguration:serviceConfiguration + idTokenHint:requestParameters.idTokenHint + postLogoutRedirectURL:postLogoutRedirectURL + state:resolvedState + additionalParameters:requestParameters.additionalParameters]; UIViewController *rootViewController = [self rootViewController]; id externalUserAgent = 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..c958b478 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppAuth.h @@ -50,7 +50,7 @@ static NSString *const END_SESSION_ERROR_MESSAGE_FORMAT = @interface EndSessionRequestParameters : NSObject @property(nonatomic, strong) NSString *idTokenHint; @property(nonatomic, strong) NSString *postLogoutRedirectUrl; -@property(nonatomic, strong) NSString *state; +@property(nonatomic, strong) id state; @property(nonatomic, strong) NSString *issuer; @property(nonatomic, strong) NSString *discoveryUrl; @property(nonatomic, strong) NSDictionary *serviceConfigurationParameters; @@ -76,7 +76,8 @@ typedef NS_ENUM(NSInteger, ExternalUserAgent) { externalUserAgent:(NSNumber *)externalUserAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode - nonce:(NSString *)nonce; + nonce:(NSString *)nonce + state:(id)state; - (id) performEndSessionRequest:(OIDServiceConfiguration *)serviceConfiguration 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..e416d52d 100644 --- a/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m +++ b/flutter_appauth/ios/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m @@ -80,6 +80,7 @@ @interface AuthorizationTokenRequestParameters : TokenRequestParameters @property(nonatomic, strong) NSString *loginHint; @property(nonatomic, strong) NSArray *promptValues; @property(nonatomic, strong) NSString *responseMode; +@property(nonatomic, strong) id state; // nil = absent (auto-generate), NSNull = suppress, NSString = custom @end @implementation AuthorizationTokenRequestParameters @@ -91,6 +92,7 @@ - (id)initWithArguments:(NSDictionary *)arguments { withKey:@"promptValues"]; _responseMode = [ArgumentProcessor processArgumentValue:arguments withKey:@"responseMode"]; + _state = [arguments objectForKey:@"state"]; return self; } @end @@ -102,7 +104,7 @@ - (id)initWithArguments:(NSDictionary *)arguments { _postLogoutRedirectUrl = [ArgumentProcessor processArgumentValue:arguments withKey:@"postLogoutRedirectUrl"]; - _state = [ArgumentProcessor processArgumentValue:arguments withKey:@"state"]; + _state = [arguments objectForKey:@"state"]; _issuer = [ArgumentProcessor processArgumentValue:arguments withKey:@"issuer"]; _discoveryUrl = [ArgumentProcessor processArgumentValue:arguments @@ -211,7 +213,8 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments externalUserAgent:requestParameters.externalUserAgent result:result exchangeCode:exchangeCode - nonce:requestParameters.nonce]; + nonce:requestParameters.nonce + state:requestParameters.state]; } else if (requestParameters.discoveryUrl) { NSURL *discoveryUrl = [NSURL URLWithString:requestParameters.discoveryUrl]; [OIDAuthorizationService @@ -253,7 +256,10 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments exchangeCode:exchangeCode nonce: requestParameters - .nonce]; + .nonce + state: + requestParameters + .state]; }]; } else { NSURL *issuerUrl = [NSURL URLWithString:requestParameters.issuer]; @@ -293,7 +299,10 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments exchangeCode:exchangeCode nonce: requestParameters - .nonce]; + .nonce + state: + requestParameters + .state]; }]; } } 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..bf6107bc 100644 --- a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/AppAuthMacOSAuthorization.m @@ -12,10 +12,14 @@ @implementation AppAuthMacOSAuthorization externalUserAgent:(NSNumber *)externalUserAgent result:(FlutterResult)result exchangeCode:(BOOL)exchangeCode - nonce:(NSString *)nonce { + nonce:(NSString *)nonce + state:(id)state { NSString *codeVerifier = [OIDAuthorizationRequest generateCodeVerifier]; NSString *codeChallenge = [OIDAuthorizationRequest codeChallengeS256ForVerifier:codeVerifier]; + NSString *resolvedState = (state != nil) + ? ([state isKindOfClass:[NSString class]] ? state : nil) + : [OIDAuthorizationRequest generateState]; OIDAuthorizationRequest *request = [[OIDAuthorizationRequest alloc] initWithConfiguration:serviceConfiguration @@ -24,7 +28,7 @@ @implementation AppAuthMacOSAuthorization scope:[OIDScopeUtilities scopesWithArray:scopes] redirectURL:[NSURL URLWithString:redirectUrl] responseType:OIDResponseTypeCode - state:[OIDAuthorizationRequest generateState] + state:resolvedState nonce:nonce != nil ? nonce : [OIDAuthorizationRequest generateState] @@ -119,19 +123,18 @@ @implementation AppAuthMacOSAuthorization ? [NSURL URLWithString:requestParameters.postLogoutRedirectUrl] : nil; + NSString *resolvedState = (requestParameters.state != nil) + ? ([requestParameters.state isKindOfClass:[NSString class]] + ? requestParameters.state + : nil) + : [OIDAuthorizationRequest generateState]; 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]; + [[OIDEndSessionRequest alloc] + initWithConfiguration:serviceConfiguration + idTokenHint:requestParameters.idTokenHint + postLogoutRedirectURL:postLogoutRedirectURL + state:resolvedState + additionalParameters:requestParameters.additionalParameters]; NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow]; id externalUserAgent = 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..1d10078c 100644 --- a/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m +++ b/flutter_appauth/macos/flutter_appauth/Sources/flutter_appauth/FlutterAppauthPlugin.m @@ -80,6 +80,8 @@ @interface AuthorizationTokenRequestParameters : TokenRequestParameters @property(nonatomic, strong) NSString *loginHint; @property(nonatomic, strong) NSArray *promptValues; @property(nonatomic, strong) NSString *responseMode; +/// Raw value from the method channel: nil = absent (auto-generate), NSNull = suppress, NSString = custom. +@property(nonatomic, strong) id state; @end @implementation AuthorizationTokenRequestParameters @@ -91,6 +93,7 @@ - (id)initWithArguments:(NSDictionary *)arguments { withKey:@"promptValues"]; _responseMode = [ArgumentProcessor processArgumentValue:arguments withKey:@"responseMode"]; + _state = [arguments objectForKey:@"state"]; return self; } @end @@ -102,7 +105,7 @@ - (id)initWithArguments:(NSDictionary *)arguments { _postLogoutRedirectUrl = [ArgumentProcessor processArgumentValue:arguments withKey:@"postLogoutRedirectUrl"]; - _state = [ArgumentProcessor processArgumentValue:arguments withKey:@"state"]; + _state = [arguments objectForKey:@"state"]; _issuer = [ArgumentProcessor processArgumentValue:arguments withKey:@"issuer"]; _discoveryUrl = [ArgumentProcessor processArgumentValue:arguments @@ -210,7 +213,8 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments externalUserAgent:requestParameters.externalUserAgent result:result exchangeCode:exchangeCode - nonce:requestParameters.nonce]; + nonce:requestParameters.nonce + state:requestParameters.state]; } else if (requestParameters.discoveryUrl) { NSURL *discoveryUrl = [NSURL URLWithString:requestParameters.discoveryUrl]; [OIDAuthorizationService @@ -252,7 +256,10 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments exchangeCode:exchangeCode nonce: requestParameters - .nonce]; + .nonce + state: + requestParameters + .state]; }]; } else { NSURL *issuerUrl = [NSURL URLWithString:requestParameters.issuer]; @@ -292,7 +299,10 @@ - (void)handleAuthorizeMethodCall:(NSDictionary *)arguments exchangeCode:exchangeCode nonce: requestParameters - .nonce]; + .nonce + state: + requestParameters + .state]; }]; } } 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 7039340c..ebd0c324 100644 --- a/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart +++ b/flutter_appauth_platform_interface/lib/flutter_appauth_platform_interface.dart @@ -5,6 +5,7 @@ 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/request_state.dart'; export 'src/errors.dart'; export 'src/external_user_agent.dart'; export 'src/flutter_appauth_platform.dart'; diff --git a/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart b/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart index 88572d66..3df26fed 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_parameters.dart @@ -1,4 +1,5 @@ import 'external_user_agent.dart'; +import 'request_state.dart'; mixin AuthorizationParameters { /// Hint to the Authorization Server about the login identifier the End-User @@ -14,4 +15,15 @@ mixin AuthorizationParameters { /// Specifies the response mode to use. String? responseMode; + + /// Controls the `state` parameter for this authorization request. + /// + /// Defaults to [AutoGeneratedState], which lets the native AppAuth SDK + /// generate a cryptographically random value. + /// + /// See also: + /// * [SuppressedState] to omit the state parameter entirely (removes CSRF + /// protection — use with care) + /// * [CustomState] to provide an explicit value + RequestState state = const AutoGeneratedState(); } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_request.dart index a0a2fd3a..8c034ac5 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_request.dart @@ -2,6 +2,7 @@ import 'authorization_parameters.dart'; import 'authorization_service_configuration.dart'; import 'common_request_details.dart'; import 'external_user_agent.dart'; +import 'request_state.dart'; /// The details of an authorization request to get an authorization code. class AuthorizationRequest extends CommonRequestDetails @@ -21,6 +22,7 @@ class AuthorizationRequest extends CommonRequestDetails ExternalUserAgent.asWebAuthenticationSession, String? nonce, String? responseMode, + RequestState state = const AutoGeneratedState(), }) { this.clientId = clientId; this.redirectUrl = redirectUrl; @@ -35,6 +37,7 @@ class AuthorizationRequest extends CommonRequestDetails this.externalUserAgent = externalUserAgent; this.nonce = nonce; this.responseMode = responseMode; + this.state = state; assertConfigurationInfo(); } } diff --git a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart index 8436c45d..d721419e 100644 --- a/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart +++ b/flutter_appauth_platform_interface/lib/src/authorization_token_request.dart @@ -1,6 +1,7 @@ import 'authorization_parameters.dart'; import 'external_user_agent.dart'; import 'grant_type.dart'; +import 'request_state.dart'; import 'token_request.dart'; /// Details required for a combined authorization and code exchange request @@ -22,6 +23,7 @@ class AuthorizationTokenRequest extends TokenRequest ExternalUserAgent.asWebAuthenticationSession, super.nonce, String? responseMode, + RequestState state = const AutoGeneratedState(), }) : super( grantType: GrantType.authorizationCode, ) { @@ -29,5 +31,6 @@ class AuthorizationTokenRequest extends TokenRequest this.promptValues = promptValues; this.externalUserAgent = externalUserAgent; this.responseMode = responseMode; + this.state = state; } } diff --git a/flutter_appauth_platform_interface/lib/src/end_session_request.dart b/flutter_appauth_platform_interface/lib/src/end_session_request.dart index 60213fc5..27603b56 100644 --- a/flutter_appauth_platform_interface/lib/src/end_session_request.dart +++ b/flutter_appauth_platform_interface/lib/src/end_session_request.dart @@ -6,7 +6,7 @@ class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { EndSessionRequest({ this.idTokenHint, this.postLogoutRedirectUrl, - this.state, + this.state = const AutoGeneratedState(), this.allowInsecureConnections = false, this.externalUserAgent = ExternalUserAgent.asWebAuthenticationSession, this.additionalParameters, @@ -31,7 +31,15 @@ class EndSessionRequest with AcceptedAuthorizationServiceConfigurationDetails { /// When specified, the [idTokenHint] must also be provided. final String? postLogoutRedirectUrl; - final String? state; + /// Controls the `state` parameter for this end-session request. + /// + /// Defaults to [AutoGeneratedState], which lets the native AppAuth SDK + /// generate a cryptographically random value. + /// + /// See also: + /// * [SuppressedState] to omit the state parameter entirely + /// * [CustomState] to provide an explicit value + final RequestState state; /// Whether to allow non-HTTPS endpoints. /// 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..8b50cd75 100644 --- a/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart +++ b/flutter_appauth_platform_interface/lib/src/method_channel_mappers.dart @@ -5,6 +5,7 @@ import 'authorization_token_request.dart'; import 'common_request_details.dart'; import 'end_session_request.dart'; import 'grant_type.dart'; +import 'request_state.dart'; import 'token_request.dart'; Map _convertCommonRequestDetailsToMap( @@ -27,7 +28,7 @@ extension EndSessionRequestMapper on EndSessionRequest { return { 'idTokenHint': idTokenHint, 'postLogoutRedirectUrl': postLogoutRedirectUrl, - 'state': state, + if (state is! AutoGeneratedState) 'state': state.value, 'allowInsecureConnections': allowInsecureConnections, 'additionalParameters': additionalParameters, 'issuer': issuer, @@ -101,5 +102,7 @@ Map _convertAuthorizationParametersToMap( 'promptValues': authorizationParameters.promptValues, 'externalUserAgent': authorizationParameters.externalUserAgent?.index, 'responseMode': authorizationParameters.responseMode, + if (authorizationParameters.state is! AutoGeneratedState) + 'state': authorizationParameters.state.value, }; } diff --git a/flutter_appauth_platform_interface/lib/src/request_state.dart b/flutter_appauth_platform_interface/lib/src/request_state.dart new file mode 100644 index 00000000..d5fd7ca0 --- /dev/null +++ b/flutter_appauth_platform_interface/lib/src/request_state.dart @@ -0,0 +1,45 @@ +/// Controls the `state` parameter sent in OAuth OpenID requests. +/// +/// Use [AutoGeneratedState] (the default) to let the native AppAuth SDK +/// generate a cryptographically random value, [SuppressedState] to omit it +/// entirely, or [CustomState] to provide an explicit value. +sealed class RequestState { + const RequestState(); + + /// The underlying state value to send. + String? get value; +} + +/// The native AppAuth SDKs generate a cryptographically random state value. +/// +/// This is the default behaviour and is recommended for most use-cases. +/// +/// The [value] is always null, since its a placeholder that shall never be +/// provided to the native SDKs. +class AutoGeneratedState extends RequestState { + const AutoGeneratedState(); + + @override + final String? value = null; +} + +/// No state parameter is sent to the IdP. +/// +/// Use this when the Identity Provider rejects requests that include a state +/// value or does not echo it back, causing response validation to fail. +class SuppressedState extends RequestState { + const SuppressedState(); + + @override + final String? value = null; +} + +/// An explicit state value provided by the caller. +class CustomState extends RequestState { + const CustomState(this._value) : assert(_value != '', 'state must not be empty'); + + final String _value; + + @override + String? get value => _value; +} 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..656b85bd 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 @@ -48,6 +48,66 @@ void main() { ); }); + test('authorize with CustomState', () async { + await flutterAppAuth.authorize(AuthorizationRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + loginHint: 'someLoginHint', + state: const CustomState('someState'))); + expect( + log, + [ + isMethodCall('authorize', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': 'someLoginHint', + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'responseMode': null, + 'nonce': null, + 'state': 'someState', + }) + ], + ); + }); + + test('authorize with SuppressedState sends null state', () async { + await flutterAppAuth.authorize(AuthorizationRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + loginHint: 'someLoginHint', + state: const SuppressedState())); + expect( + log, + [ + isMethodCall('authorize', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': 'someLoginHint', + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'responseMode': null, + 'nonce': null, + 'state': null, + }) + ], + ); + }); + test('authorizeAndExchangeCode', () async { await flutterAppAuth.authorizeAndExchangeCode(AuthorizationTokenRequest( 'someClientId', 'someRedirectUrl', @@ -82,6 +142,79 @@ void main() { ); }); + test('authorizeAndExchangeCode with CustomState', () async { + await flutterAppAuth.authorizeAndExchangeCode(AuthorizationTokenRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + loginHint: 'someLoginHint', + responseMode: 'fragment', + state: const CustomState('someState'))); + expect( + log, + [ + isMethodCall('authorizeAndExchangeCode', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': 'someLoginHint', + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'clientSecret': null, + 'refreshToken': null, + 'authorizationCode': null, + 'grantType': 'authorization_code', + 'codeVerifier': null, + 'responseMode': 'fragment', + 'nonce': null, + 'state': 'someState', + }) + ], + ); + }); + + test('authorizeAndExchangeCode with SuppressedState sends null state', + () async { + await flutterAppAuth.authorizeAndExchangeCode(AuthorizationTokenRequest( + 'someClientId', 'someRedirectUrl', + discoveryUrl: 'someDiscoveryUrl', + loginHint: 'someLoginHint', + responseMode: 'fragment', + state: const SuppressedState())); + expect( + log, + [ + isMethodCall('authorizeAndExchangeCode', arguments: { + 'clientId': 'someClientId', + 'issuer': null, + 'redirectUrl': 'someRedirectUrl', + 'discoveryUrl': 'someDiscoveryUrl', + 'loginHint': 'someLoginHint', + 'scopes': null, + 'serviceConfiguration': null, + 'additionalParameters': null, + 'allowInsecureConnections': false, + 'externalUserAgent': + ExternalUserAgent.asWebAuthenticationSession.index, + 'promptValues': null, + 'clientSecret': null, + 'refreshToken': null, + 'authorizationCode': null, + 'grantType': 'authorization_code', + 'codeVerifier': null, + 'responseMode': 'fragment', + 'nonce': null, + 'state': null, + }) + ], + ); + }); + group('token', () { test('cannot infer grant type', () async { expect( @@ -170,11 +303,30 @@ void main() { }); }); - test('endSession', () async { + test('endSession with default AutoGeneratedState omits state key', () async { + await flutterAppAuth.endSession(EndSessionRequest( + idTokenHint: 'someIdToken', + postLogoutRedirectUrl: 'somePostLogoutRedirectUrl', + discoveryUrl: 'someDiscoveryUrl')); + expect(log, [ + isMethodCall('endSession', arguments: { + 'idTokenHint': 'someIdToken', + 'postLogoutRedirectUrl': 'somePostLogoutRedirectUrl', + 'allowInsecureConnections': false, + 'additionalParameters': null, + 'issuer': null, + 'discoveryUrl': 'someDiscoveryUrl', + 'serviceConfiguration': null, + 'externalUserAgent': ExternalUserAgent.asWebAuthenticationSession.index, + }) + ]); + }); + + test('endSession with CustomState', () async { await flutterAppAuth.endSession(EndSessionRequest( idTokenHint: 'someIdToken', postLogoutRedirectUrl: 'somePostLogoutRedirectUrl', - state: 'someState', + state: const CustomState('someState'), discoveryUrl: 'someDiscoveryUrl')); expect(log, [ isMethodCall('endSession', arguments: { @@ -190,4 +342,25 @@ void main() { }) ]); }); + + test('endSession with SuppressedState sends null state', () async { + await flutterAppAuth.endSession(EndSessionRequest( + idTokenHint: 'someIdToken', + postLogoutRedirectUrl: 'somePostLogoutRedirectUrl', + state: const SuppressedState(), + discoveryUrl: 'someDiscoveryUrl')); + expect(log, [ + isMethodCall('endSession', arguments: { + 'idTokenHint': 'someIdToken', + 'postLogoutRedirectUrl': 'somePostLogoutRedirectUrl', + 'state': null, + 'allowInsecureConnections': false, + 'additionalParameters': null, + 'issuer': null, + 'discoveryUrl': 'someDiscoveryUrl', + 'serviceConfiguration': null, + 'externalUserAgent': ExternalUserAgent.asWebAuthenticationSession.index, + }) + ]); + }); }