diff --git a/googleapis_auth/CHANGELOG.md b/googleapis_auth/CHANGELOG.md index 8f8cddd57..1dae372f3 100644 --- a/googleapis_auth/CHANGELOG.md +++ b/googleapis_auth/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.5.0 + +- Add support for non-Google OAuth 2.0 providers. + ## 1.4.2 - Require Dart 3.2 or later. diff --git a/googleapis_auth/README.md b/googleapis_auth/README.md index 6af278e6a..4760dd260 100644 --- a/googleapis_auth/README.md +++ b/googleapis_auth/README.md @@ -323,6 +323,38 @@ var client = clientViaApiKey(''); client.close(); ``` +### Using a non-Google authentication provider + +This package is designed to work with Google's OAuth flow, but it can be used +with other OAuth providers as well. To do this, you need to subclass +`AuthEndpoints` and provide authorization and token uris. For example: + +```dart +import 'package:googleapis_auth/auth_io.dart'; + +class MicrosoftAuthEndpoints extends AuthEndpoints { + @override + Uri get authorizationEndpoint => + Uri.https('login.microsoftonline.com', 'common/oauth2/v2.0/authorize'); + + @override + Uri get tokenEndpoint => + Uri.https('login.microsoftonline.com', 'common/oauth2/v2.0/token'); +} +``` + +This can then be used to obtain credentials: + +```dart +final credentials = await obtainAccessCredentialsViaUserConsent( + clientId, + ['scope1', 'scope2'], + client, + prompt, + authEndpoints: MicrosoftAuthEndpoints(), +); +``` + ### More information More information can be obtained from official Google Developers documentation: diff --git a/googleapis_auth/lib/auth_io.dart b/googleapis_auth/lib/auth_io.dart index f99f70e85..4400d566a 100644 --- a/googleapis_auth/lib/auth_io.dart +++ b/googleapis_auth/lib/auth_io.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'package:http/http.dart'; import 'src/adc_utils.dart'; +import 'src/auth_endpoints.dart'; import 'src/auth_http_utils.dart'; import 'src/http_client_base.dart'; import 'src/metadata_server_client.dart' show clientViaMetadataServer; @@ -122,6 +123,7 @@ Future clientViaUserConsent( Client? baseClient, String? hostedDomain, int listenPort = 0, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), }) async { var closeUnderlyingClient = false; if (baseClient == null) { @@ -130,6 +132,7 @@ Future clientViaUserConsent( } final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, clientId, scopes, baseClient, @@ -150,6 +153,7 @@ Future clientViaUserConsent( } return AutoRefreshingClient( baseClient, + authEndpoints, clientId, credentials, closeUnderlyingClient: closeUnderlyingClient, @@ -177,6 +181,7 @@ Future clientViaUserConsentManual( PromptUserForConsentManual userPrompt, { Client? baseClient, String? hostedDomain, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), }) async { var closeUnderlyingClient = false; if (baseClient == null) { @@ -185,6 +190,7 @@ Future clientViaUserConsentManual( } final flow = AuthorizationCodeGrantManualFlow( + authEndpoints, clientId, scopes, baseClient, @@ -205,6 +211,7 @@ Future clientViaUserConsentManual( return AutoRefreshingClient( baseClient, + authEndpoints, clientId, credentials, closeUnderlyingClient: closeUnderlyingClient, @@ -238,8 +245,10 @@ Future obtainAccessCredentialsViaUserConsent( PromptUserForConsent userPrompt, { String? hostedDomain, int listenPort = 0, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), }) => AuthorizationCodeGrantServerFlow( + authEndpoints, clientId, scopes, client, @@ -266,8 +275,10 @@ Future obtainAccessCredentialsViaUserConsentManual( Client client, PromptUserForConsentManual userPrompt, { String? hostedDomain, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), }) => AuthorizationCodeGrantManualFlow( + authEndpoints, clientId, scopes, client, diff --git a/googleapis_auth/lib/googleapis_auth.dart b/googleapis_auth/lib/googleapis_auth.dart index ce2033afc..50a68754e 100644 --- a/googleapis_auth/lib/googleapis_auth.dart +++ b/googleapis_auth/lib/googleapis_auth.dart @@ -26,6 +26,7 @@ library googleapis_auth; export 'src/auth_client.dart'; +export 'src/auth_endpoints.dart'; export 'src/auth_functions.dart'; export 'src/client_id.dart'; export 'src/exceptions.dart'; diff --git a/googleapis_auth/lib/src/adc_utils.dart b/googleapis_auth/lib/src/adc_utils.dart index e44b4f5a5..9b361d638 100644 --- a/googleapis_auth/lib/src/adc_utils.dart +++ b/googleapis_auth/lib/src/adc_utils.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:http/http.dart'; +import 'auth_endpoints.dart'; import 'auth_functions.dart'; import 'auth_http_utils.dart'; import 'service_account_client.dart'; @@ -39,6 +40,7 @@ Future fromApplicationsCredentialsFile( ); return AutoRefreshingClient( baseClient, + const GoogleAuthEndpoints(), clientId, await refreshCredentials( clientId, diff --git a/googleapis_auth/lib/src/auth_endpoints.dart b/googleapis_auth/lib/src/auth_endpoints.dart new file mode 100644 index 000000000..7dea613a6 --- /dev/null +++ b/googleapis_auth/lib/src/auth_endpoints.dart @@ -0,0 +1,34 @@ +import 'known_uris.dart'; + +/// {@template AuthEndpoints} +/// The endpoints required for an OAuth 2.0 authorization code flow. +/// {@endtemplate} +abstract class AuthEndpoints { + /// {@macro AuthEndpoints} + const AuthEndpoints(); + + /// The OAuth endpoint used to obtain a single-use authorization code. + Uri get authorizationEndpoint; + + /// The OAuth endpoint used to exchange an authorization code for access + /// credentials. + Uri get tokenEndpoint; +} + +/// {@template GoogleAuthEndpoints} +/// The endpoints required for an OAuth 2.0 authorization code flow with Google. +/// +/// This is the only implementation of [AuthEndpoints] provided by this package. +/// Package consumers can provide their own implementation if they are using a +/// different OAuth 2.0 provider or providers. +/// {@endtemplate} +class GoogleAuthEndpoints extends AuthEndpoints { + /// {@macro GoogleAuthEndpoints} + const GoogleAuthEndpoints(); + + @override + Uri get authorizationEndpoint => googleOauth2AuthorizationEndpoint; + + @override + Uri get tokenEndpoint => googleOauth2TokenEndpoint; +} diff --git a/googleapis_auth/lib/src/auth_functions.dart b/googleapis_auth/lib/src/auth_functions.dart index 203fc7389..244a9b3b1 100644 --- a/googleapis_auth/lib/src/auth_functions.dart +++ b/googleapis_auth/lib/src/auth_functions.dart @@ -8,6 +8,7 @@ import 'package:http/http.dart'; import 'access_credentials.dart'; import 'auth_client.dart'; +import 'auth_endpoints.dart'; import 'auth_http_utils.dart'; import 'client_id.dart'; import 'http_client_base.dart'; @@ -84,15 +85,16 @@ AuthClient authenticatedClient( AutoRefreshingAuthClient autoRefreshingClient( ClientId clientId, AccessCredentials credentials, - Client baseClient, -) { + Client baseClient, { + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), +}) { if (credentials.accessToken.type != 'Bearer') { throw ArgumentError('Only Bearer access tokens are accepted.'); } if (credentials.refreshToken == null) { throw ArgumentError('Refresh token in AccessCredentials was `null`.'); } - return AutoRefreshingClient(baseClient, clientId, credentials); + return AutoRefreshingClient(baseClient, authEndpoints, clientId, credentials); } /// Obtains refreshed [AccessCredentials] for [clientId] and [credentials]. @@ -103,25 +105,26 @@ AutoRefreshingAuthClient autoRefreshingClient( Future refreshCredentials( ClientId clientId, AccessCredentials credentials, - Client client, -) async { - final secret = clientId.secret; - if (secret == null) { - throw ArgumentError('clientId.secret cannot be null.'); - } - + Client client, { + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), +}) async { final refreshToken = credentials.refreshToken; if (refreshToken == null) { throw ArgumentError('clientId.refreshToken cannot be null.'); } // https://developers.google.com/identity/protocols/oauth2/native-app#offline - final jsonMap = await client.oauthTokenRequest({ - 'client_id': clientId.identifier, - 'client_secret': secret, - 'refresh_token': refreshToken, - 'grant_type': 'refresh_token', - }); + final jsonMap = await client.oauthTokenRequest( + { + 'client_id': clientId.identifier, + // Not all providers require a client secret, + // e.g. https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token + if (clientId.secret != null) 'client_secret': clientId.secret!, + 'refresh_token': refreshToken, + 'grant_type': 'refresh_token', + }, + authEndpoints: authEndpoints, + ); final accessToken = parseAccessToken(jsonMap); diff --git a/googleapis_auth/lib/src/auth_http_utils.dart b/googleapis_auth/lib/src/auth_http_utils.dart index 9aa64c7ba..e9721c757 100644 --- a/googleapis_auth/lib/src/auth_http_utils.dart +++ b/googleapis_auth/lib/src/auth_http_utils.dart @@ -8,6 +8,7 @@ import 'package:http/http.dart'; import 'access_credentials.dart'; import 'auth_client.dart'; +import 'auth_endpoints.dart'; import 'auth_functions.dart'; import 'client_id.dart'; import 'exceptions.dart'; @@ -91,9 +92,11 @@ class AutoRefreshingClient extends AutoRefreshDelegatingClient { @override AccessCredentials credentials; late Client authClient; + final AuthEndpoints authEndpoints; AutoRefreshingClient( super.client, + this.authEndpoints, this.clientId, this.credentials, { super.closeUnderlyingClient, @@ -114,7 +117,12 @@ class AutoRefreshingClient extends AutoRefreshDelegatingClient { // If so, we should handle it. return authClient.send(request); } else { - final cred = await refreshCredentials(clientId, credentials, baseClient); + final cred = await refreshCredentials( + clientId, + credentials, + baseClient, + authEndpoints: authEndpoints, + ); notifyAboutNewCredentials(cred); credentials = cred; authClient = AuthenticatedClient( diff --git a/googleapis_auth/lib/src/oauth2_flows/auth_code.dart b/googleapis_auth/lib/src/oauth2_flows/auth_code.dart index 5d2142723..2c2f82067 100644 --- a/googleapis_auth/lib/src/oauth2_flows/auth_code.dart +++ b/googleapis_auth/lib/src/oauth2_flows/auth_code.dart @@ -11,9 +11,9 @@ import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import '../access_credentials.dart'; +import '../auth_endpoints.dart'; import '../client_id.dart'; import '../exceptions.dart'; -import '../known_uris.dart'; import '../utils.dart'; Uri createAuthenticationUri({ @@ -24,6 +24,7 @@ Uri createAuthenticationUri({ String? hostedDomain, String? state, bool offline = false, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), }) { final queryValues = { 'client_id': clientId, @@ -36,7 +37,7 @@ Uri createAuthenticationUri({ if (hostedDomain != null) 'hd': hostedDomain, if (state != null) 'state': state, }; - return googleOauth2AuthorizationEndpoint.replace( + return authEndpoints.authorizationEndpoint.replace( queryParameters: queryValues, ); } @@ -111,6 +112,7 @@ Future obtainAccessCredentialsViaCodeExchange( String code, { String redirectUrl = 'postmessage', String? codeVerifier, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), }) async { final jsonMap = await client.oauthTokenRequest( { @@ -121,6 +123,7 @@ Future obtainAccessCredentialsViaCodeExchange( 'grant_type': 'authorization_code', 'redirect_uri': redirectUrl, }, + authEndpoints: authEndpoints, ); final accessToken = parseAccessToken(jsonMap); diff --git a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart index 350b5199f..62407a7fc 100644 --- a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart +++ b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_abstract_flow.dart @@ -5,17 +5,20 @@ import 'package:http/http.dart' as http; import '../access_credentials.dart'; +import '../auth_endpoints.dart'; import '../client_id.dart'; import 'auth_code.dart'; import 'base_flow.dart'; abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow { + final AuthEndpoints authEndpoints; final ClientId clientId; final String? hostedDomain; final List scopes; final http.Client _client; AuthorizationCodeGrantAbstractFlow( + this.authEndpoints, this.clientId, this.scopes, this._client, { @@ -25,6 +28,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow { Future obtainAccessCredentialsUsingCodeImpl( String code, String redirectUri, { + required AuthEndpoints authEndpoints, required String codeVerifier, }) => obtainAccessCredentialsViaCodeExchange( @@ -33,6 +37,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow { code, redirectUrl: redirectUri, codeVerifier: codeVerifier, + authEndpoints: authEndpoints, ); Uri authenticationUri( @@ -41,6 +46,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow { required String codeVerifier, }) => createAuthenticationUri( + authEndpoints: authEndpoints, redirectUri: redirectUri, clientId: clientId.identifier, scopes: scopes, diff --git a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart index 2613795a7..f0a10eb2f 100644 --- a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart +++ b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_manual_flow.dart @@ -24,6 +24,7 @@ class AuthorizationCodeGrantManualFlow final PromptUserForConsentManual userPrompt; AuthorizationCodeGrantManualFlow( + super.authEndpoints, super.clientId, super.scopes, super.client, @@ -47,6 +48,7 @@ class AuthorizationCodeGrantManualFlow return obtainAccessCredentialsUsingCodeImpl( code, _redirectionUri, + authEndpoints: authEndpoints, codeVerifier: codeVerifier, ); } diff --git a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart index 52817ac50..a4eed091f 100644 --- a/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart +++ b/googleapis_auth/lib/src/oauth2_flows/authorization_code_grant_server_flow.dart @@ -27,6 +27,7 @@ class AuthorizationCodeGrantServerFlow final int listenPort; AuthorizationCodeGrantServerFlow( + super.authEndpoints, super.clientId, super.scopes, super.client, @@ -89,6 +90,7 @@ class AuthorizationCodeGrantServerFlow final credentials = await obtainAccessCredentialsUsingCodeImpl( code, redirectionUri, + authEndpoints: authEndpoints, codeVerifier: codeVerifier, ); diff --git a/googleapis_auth/lib/src/oauth2_flows/jwt.dart b/googleapis_auth/lib/src/oauth2_flows/jwt.dart index 1d3eba99e..09f6b83eb 100644 --- a/googleapis_auth/lib/src/oauth2_flows/jwt.dart +++ b/googleapis_auth/lib/src/oauth2_flows/jwt.dart @@ -8,6 +8,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import '../access_credentials.dart'; +import '../auth_endpoints.dart'; import '../crypto/rsa.dart'; import '../crypto/rsa_sign.dart'; import '../known_uris.dart'; @@ -58,10 +59,13 @@ class JwtFlow extends BaseFlow { final jwt = '$jwtSignatureInput.${_base64url(signature)}'; // https://developers.google.com/identity/protocols/oauth2/service-account#authorizingrequests - final response = await _client.oauthTokenRequest({ - 'grant_type': _uri, - 'assertion': jwt, - }); + final response = await _client.oauthTokenRequest( + { + 'grant_type': _uri, + 'assertion': jwt, + }, + authEndpoints: const GoogleAuthEndpoints(), + ); final accessToken = parseAccessToken(response); return AccessCredentials(accessToken, null, _scopes); } diff --git a/googleapis_auth/lib/src/utils.dart b/googleapis_auth/lib/src/utils.dart index 785473bbe..f28969a09 100644 --- a/googleapis_auth/lib/src/utils.dart +++ b/googleapis_auth/lib/src/utils.dart @@ -8,9 +8,9 @@ import 'package:http/http.dart' show BaseRequest, Client, StreamedResponse; import 'package:http_parser/http_parser.dart'; import 'access_token.dart'; +import 'auth_endpoints.dart'; import 'exceptions.dart'; import 'http_client_base.dart'; -import 'known_uris.dart'; /// Due to differences of clock speed, network latency, etc. we /// will shorten expiry dates by 20 seconds. @@ -109,8 +109,9 @@ extension ClientExtensions on Client { } Future> oauthTokenRequest( - Map postValues, - ) async { + Map postValues, { + required AuthEndpoints authEndpoints, + }) async { final body = Stream>.value( ascii.encode( postValues.entries @@ -118,7 +119,7 @@ extension ClientExtensions on Client { .join('&'), ), ); - final request = RequestImpl('POST', googleOauth2TokenEndpoint, body) + final request = RequestImpl('POST', authEndpoints.tokenEndpoint, body) ..headers['content-type'] = _contentTypeUrlEncoded; return requestJson(request, 'Failed to obtain access credentials.'); diff --git a/googleapis_auth/pubspec.yaml b/googleapis_auth/pubspec.yaml index cc790d5ea..c51b3a78d 100644 --- a/googleapis_auth/pubspec.yaml +++ b/googleapis_auth/pubspec.yaml @@ -1,5 +1,5 @@ name: googleapis_auth -version: 1.4.2 +version: 1.5.0 description: Obtain Access credentials for Google services using OAuth 2.0 repository: https://github.com/google/googleapis.dart/tree/master/googleapis_auth @@ -23,4 +23,4 @@ dev_dependencies: test: ^1.16.0 false_secrets: -- test/test_utils.dart + - test/test_utils.dart diff --git a/googleapis_auth/test/oauth2_flows/auth_code_test.dart b/googleapis_auth/test/oauth2_flows/auth_code_test.dart index daa7819f7..78f2d03e5 100644 --- a/googleapis_auth/test/oauth2_flows/auth_code_test.dart +++ b/googleapis_auth/test/oauth2_flows/auth_code_test.dart @@ -17,6 +17,14 @@ import '../test_utils.dart'; typedef RequestHandler = Future Function(Request _); +class CustomAuthEndpoints extends AuthEndpoints { + @override + Uri get authorizationEndpoint => Uri.https('example.com', '/auth'); + + @override + Uri get tokenEndpoint => Uri.https('example.com', '/token'); +} + final _browserFlowRedirectMatcher = predicate((object) { if (object.startsWith('redirect_uri=')) { final url = Uri.parse( @@ -31,13 +39,17 @@ final _browserFlowRedirectMatcher = predicate((object) { void main() { final clientId = ClientId('id', 'secret'); final scopes = ['s1', 's2']; + const authEndpoints = GoogleAuthEndpoints(); // Validation + Responses from the authorization server. - RequestHandler successFullResponse({required bool manual}) => + RequestHandler successFullResponse({ + required bool manual, + AuthEndpoints authEndpoints = const GoogleAuthEndpoints(), + }) => (Request request) async { expect(request.method, equals('POST')); - expect(request.url, googleOauth2TokenEndpoint); + expect(request.url, authEndpoints.tokenEndpoint); expect( request.headers['content-type']!, startsWith('application/x-www-form-urlencoded'), @@ -127,6 +139,38 @@ void main() { return redirectUri; } + Uri validateUserPromptUriWithCustomEndpoints( + String url, { + bool manual = false, + }) { + final uri = Uri.parse(url); + final authEndpoints = CustomAuthEndpoints(); + expect(uri.scheme, authEndpoints.authorizationEndpoint.scheme); + expect(uri.authority, authEndpoints.authorizationEndpoint.authority); + expect(uri.path, authEndpoints.authorizationEndpoint.path); + expect(uri.queryParameters, { + 'client_id': clientId.identifier, + 'response_type': 'code', + 'scope': 's1 s2', + 'redirect_uri': isNotEmpty, + 'code_challenge': hasLength(43), + 'code_challenge_method': 'S256', + if (!manual) 'state': hasLength(32), + }); + + final redirectUri = Uri.parse(uri.queryParameters['redirect_uri']!); + + if (manual) { + expect('$redirectUri', equals('urn:ietf:wg:oauth:2.0:oob')); + } else { + expect(uri.queryParameters['state'], isNotNull); + expect(redirectUri.scheme, equals('http')); + expect(redirectUri.host, equals('localhost')); + } + + return redirectUri; + } + group('authorization-code-flow', () { group('manual-copy-paste', () { Future manualUserPrompt(String url) async { @@ -134,8 +178,14 @@ void main() { return 'mycode'; } + Future manualUserPromptWithCustomEndpoints(String url) async { + validateUserPromptUriWithCustomEndpoints(url, manual: true); + return 'mycode'; + } + test('successful', () async { final flow = AuthorizationCodeGrantManualFlow( + authEndpoints, clientId, scopes, mockClient(successFullResponse(manual: true), expectClose: false), @@ -144,12 +194,28 @@ void main() { validateAccessCredentials(await flow.run()); }); + test('successful (custom endpoints)', () async { + final authEndpoints = CustomAuthEndpoints(); + final flow = AuthorizationCodeGrantManualFlow( + authEndpoints, + clientId, + scopes, + mockClient( + successFullResponse(manual: true, authEndpoints: authEndpoints), + expectClose: false, + ), + manualUserPromptWithCustomEndpoints, + ); + validateAccessCredentials(await flow.run()); + }); + test('user-exception', () async { // We use a TransportException here for convenience. Future manualUserPromptError(String url) => Future.error(TransportException()); final flow = AuthorizationCodeGrantManualFlow( + authEndpoints, clientId, scopes, mockClient(successFullResponse(manual: true), expectClose: false), @@ -160,6 +226,7 @@ void main() { test('transport-exception', () async { final flow = AuthorizationCodeGrantManualFlow( + authEndpoints, clientId, scopes, transportFailure, @@ -170,6 +237,7 @@ void main() { test('invalid-server-response', () async { final flow = AuthorizationCodeGrantManualFlow( + authEndpoints, clientId, scopes, mockClient(invalidResponse, expectClose: false), @@ -194,6 +262,20 @@ void main() { } } + Future postToRedirectionEndpoint(Uri authCodeCall) async { + final ioClient = HttpClient(); + + final closeMe = expectAsync0(ioClient.close); + + try { + final request = await ioClient.postUrl(authCodeCall); + final response = await request.close(); + await response.drain(); + } finally { + closeMe(); + } + } + void userPrompt(String url) { final redirectUri = validateUserPromptUri(url); final authCodeCall = Uri( @@ -208,6 +290,34 @@ void main() { callRedirectionEndpoint(authCodeCall); } + void userPromptInvalidHttpVerb(String url) { + final redirectUri = validateUserPromptUri(url); + final authCodeCall = Uri( + scheme: redirectUri.scheme, + host: redirectUri.host, + port: redirectUri.port, + path: redirectUri.path, + queryParameters: { + 'state': Uri.parse(url).queryParameters['state'], + 'code': 'mycode', + }); + postToRedirectionEndpoint(authCodeCall); + } + + void userPromptNonMatchingState(String url) { + final redirectUri = validateUserPromptUri(url); + final authCodeCall = Uri( + scheme: redirectUri.scheme, + host: redirectUri.host, + port: redirectUri.port, + path: redirectUri.path, + queryParameters: { + 'state': 'not-the-right-state', + 'code': 'mycode', + }); + callRedirectionEndpoint(authCodeCall); + } + void userPromptInvalidAuthCodeCallback(String url) { final redirectUri = validateUserPromptUri(url); final authCodeCall = Uri( @@ -224,6 +334,7 @@ void main() { test('successful', () async { final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, clientId, scopes, mockClient(successFullResponse(manual: false), expectClose: false), @@ -234,6 +345,7 @@ void main() { test('transport-exception', () async { final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, clientId, scopes, transportFailure, @@ -242,8 +354,49 @@ void main() { await expectLater(flow.run(), throwsA(isTransportException)); }); + test('non-GET request', () async { + final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, + clientId, + scopes, + mockClient(successFullResponse(manual: false), expectClose: false), + expectAsync1(userPromptInvalidHttpVerb), + ); + await expectLater( + flow.run, + throwsA( + isA().having( + (e) => e.toString(), + 'message', + '''Exception: Invalid response from server (expected GET request callback, got: POST).''', + ), + ), + ); + }); + + test('request with invalid state parameter', () async { + final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, + clientId, + scopes, + mockClient(successFullResponse(manual: false), expectClose: false), + expectAsync1(userPromptNonMatchingState), + ); + await expectLater( + flow.run, + throwsA( + isA().having( + (e) => e.toString(), + 'message', + 'Exception: Invalid response from server (state did not match).', + ), + ), + ); + }); + test('invalid-server-response', () async { final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, clientId, scopes, mockClient(invalidResponse, expectClose: false), @@ -254,6 +407,7 @@ void main() { test('failed-authentication', () async { final flow = AuthorizationCodeGrantServerFlow( + authEndpoints, clientId, scopes, mockClient(successFullResponse(manual: false), expectClose: false), diff --git a/googleapis_auth/test/oauth2_test.dart b/googleapis_auth/test/oauth2_test.dart index e6863a201..b285d2f4f 100644 --- a/googleapis_auth/test/oauth2_test.dart +++ b/googleapis_auth/test/oauth2_test.dart @@ -162,8 +162,11 @@ void main() { Future.error(Exception('transport layer exception')); test('refreshCredentials-successful', () async { - final newCredentials = await refreshCredentials(clientId, credentials, - mockClient(expectAsync1(successfulRefresh), expectClose: false)); + final newCredentials = await refreshCredentials( + clientId, + credentials, + mockClient(expectAsync1(successfulRefresh), expectClose: false), + ); final expectedResultUtc = DateTime.now() .toUtc() .add(const Duration(seconds: 3600 - maxExpectedTimeDiffInSeconds));