feat: add AuthProvider (#1743)

This commit is contained in:
Bryan Oltman
2024-02-22 10:00:52 -05:00
committed by GitHub
parent 30b1bc2f0c
commit 978381152d
27 changed files with 546 additions and 187 deletions
+82 -28
View File
@@ -8,6 +8,7 @@ import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart';
import 'package:path/path.dart' as p;
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/auth/providers/providers.dart';
import 'package:shorebird_cli/src/command.dart';
import 'package:shorebird_cli/src/command_runner.dart';
import 'package:shorebird_cli/src/http_client/http_client.dart';
@@ -20,25 +21,8 @@ final authRef = create(Auth.new);
// The [Auth] instance available in the current zone.
Auth get auth => read(authRef);
final _clientId = oauth2.ClientId(
/// Shorebird CLI's OAuth 2.0 identifier.
'523302233293-eia5antm0tgvek240t46orctktiabrek.apps.googleusercontent.com',
/// Shorebird CLI's OAuth 2.0 secret.
///
/// This isn't actually meant to be kept secret.
/// There is no way to properly secure a secret for installed/console applications.
/// Fortunately the OAuth2 flow used in this case assumes that the app cannot
/// keep secrets so this particular secret DOES NOT need to be kept secret.
/// You should however make sure not to re-use the same secret
/// anywhere secrecy is required.
///
/// For more info see: https://developers.google.com/identity/protocols/oauth2/native-app
'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',
);
final _scopes = ['openid', 'https://www.googleapis.com/auth/userinfo.email'];
typedef ObtainAccessCredentials = Future<oauth2.AccessCredentials> Function(
AuthProvider authProvider,
oauth2.ClientId clientId,
List<String> scopes,
http.Client client,
@@ -46,6 +30,7 @@ typedef ObtainAccessCredentials = Future<oauth2.AccessCredentials> Function(
);
typedef RefreshCredentials = Future<oauth2.AccessCredentials> Function(
AuthProvider authProvider,
oauth2.ClientId clientId,
oauth2.AccessCredentials credentials,
http.Client client,
@@ -104,22 +89,29 @@ class AuthenticatedClient extends http.BaseClient {
if (credentials == null) {
final token = _token!;
final jwt = Jwt.parse(token);
final authProvider = jwt.authProvider;
credentials = _credentials = await _refreshCredentials(
_clientId,
authProvider,
authProvider.clientId,
oauth2.AccessCredentials(
// This isn't relevant for a refresh operation.
AccessToken('Bearer', '', DateTime.timestamp()),
token,
_scopes,
authProvider.scopes,
),
_baseClient,
);
_onRefreshCredentials?.call(credentials);
}
if (credentials.accessToken.hasExpired) {
if (credentials.accessToken.hasExpired && credentials.idToken != null) {
final jwt = Jwt.parse(credentials.idToken!);
final authProvider = jwt.authProvider;
credentials = _credentials = await _refreshCredentials(
_clientId,
authProvider,
authProvider.clientId,
credentials,
_baseClient,
);
@@ -175,12 +167,16 @@ class Auth {
);
}
Future<AccessCredentials> loginCI(void Function(String) prompt) async {
Future<AccessCredentials> loginCI(
AuthProvider authProvider, {
required void Function(String) prompt,
}) async {
final client = http.Client();
try {
final credentials = await _obtainAccessCredentials(
_clientId,
_scopes,
authProvider,
authProvider.clientId,
authProvider.scopes,
client,
prompt,
);
@@ -201,7 +197,10 @@ class Auth {
}
}
Future<void> login(void Function(String) prompt) async {
Future<void> login(
AuthProvider authProvider, {
required void Function(String) prompt,
}) async {
if (_credentials != null) {
throw UserAlreadyLoggedInException(email: _credentials!.email!);
}
@@ -209,8 +208,9 @@ class Auth {
final client = http.Client();
try {
_credentials = await _obtainAccessCredentials(
_clientId,
_scopes,
authProvider,
authProvider.clientId,
authProvider.scopes,
client,
prompt,
);
@@ -317,3 +317,57 @@ class UserNotFoundException implements Exception {
/// credentials.
final String email;
}
extension OauthAuthProvider on Jwt {
oauth2.AuthProvider get authProvider {
if (payload.iss.startsWith('https://login.microsoftonline.com')) {
return MicrosoftAuthProvider();
} else if (payload.iss == 'https://accounts.google.com') {
return oauth2.GoogleAuthProvider();
}
throw Exception('Unknown jwt issuer: ${payload.iss}');
}
}
extension OauthValues on AuthProvider {
oauth2.ClientId get clientId {
switch (runtimeType) {
case oauth2.GoogleAuthProvider:
return oauth2.ClientId(
/// Shorebird CLI's OAuth 2.0 identifier for GCP,
'''523302233293-eia5antm0tgvek240t46orctktiabrek.apps.googleusercontent.com''',
/// Shorebird CLI's OAuth 2.0 secret for GCP.
///
/// This isn't actually meant to be kept secret.
/// There is no way to properly secure a secret for installed/console applications.
/// Fortunately the OAuth2 flow used in this case assumes that the app
/// cannot keep secrets so this particular secret DOES NOT need to be
/// kept secret. You should however make sure not to re-use the same
/// secret anywhere secrecy is required.
///
/// For more info see: https://developers.google.com/identity/protocols/oauth2/native-app
'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',
);
case MicrosoftAuthProvider:
return oauth2.ClientId(
/// Shorebird CLI's OAuth 2.0 identifier for Azure/Entra.
'c4af9566-8a36-4348-b413-dab665b8717d',
);
}
throw UnsupportedError('Unknown auth provider: $this');
}
List<String> get scopes {
switch (runtimeType) {
case oauth2.GoogleAuthProvider:
return ['openid', 'https://www.googleapis.com/auth/userinfo.email'];
case MicrosoftAuthProvider:
return ['openid'];
}
throw UnsupportedError('Unknown auth provider: $this');
}
}
@@ -0,0 +1,12 @@
import 'package:googleapis_auth/googleapis_auth.dart';
/// Endpoints for OAuth authentication with Azure/Entra/Microsoft.
class MicrosoftAuthProvider extends AuthProvider {
@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');
}
@@ -0,0 +1,2 @@
export 'package:googleapis_auth/auth_io.dart' show GoogleAuthProvider;
export 'microsoft_auth_provider.dart';
@@ -20,7 +20,7 @@ class LoginCiCommand extends ShorebirdCommand {
final AccessCredentials credentials;
try {
credentials = await auth.loginCI(prompt);
credentials = await auth.loginCI(GoogleAuthProvider(), prompt: prompt);
} on UserNotFoundException catch (error) {
logger
..err(
@@ -1,3 +1,4 @@
import 'package:googleapis_auth/auth_io.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/command.dart';
@@ -17,7 +18,7 @@ class LoginCommand extends ShorebirdCommand {
@override
Future<int> run() async {
try {
await auth.login(prompt);
await auth.login(GoogleAuthProvider(), prompt: prompt);
} on UserAlreadyLoggedInException catch (error) {
logger
..info('You are already logged in as <${error.email}>.')
+11 -11
View File
@@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: built_value
sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e
url: "https://pub.dev"
source: hosted
version: "8.9.0"
version: "8.9.1"
characters:
dependency: transitive
description:
@@ -245,10 +245,10 @@ packages:
dependency: transitive
description:
name: ffi
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
version: "2.1.2"
file:
dependency: transitive
description:
@@ -300,10 +300,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
version: "1.2.1"
http_multi_server:
dependency: transitive
description:
@@ -768,18 +768,18 @@ packages:
dependency: transitive
description:
name: web
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad"
url: "https://pub.dev"
source: hosted
version: "0.4.2"
version: "0.5.0"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23"
sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2"
url: "https://pub.dev"
source: hosted
version: "2.4.3"
version: "2.4.4"
webkit_inspection_protocol:
dependency: transitive
description:
@@ -821,4 +821,4 @@ packages:
source: hosted
version: "2.1.1"
sdks:
dart: ">=3.2.0 <4.0.0"
dart: ">=3.3.0 <4.0.0"
@@ -4,12 +4,14 @@ import 'dart:io' hide Platform;
import 'package:cli_util/cli_util.dart';
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart' show Jwt, JwtPayload;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:path/path.dart' as p;
import 'package:platform/platform.dart';
import 'package:scoped/scoped.dart';
import 'package:shorebird_cli/src/auth/auth.dart';
import 'package:shorebird_cli/src/auth/providers/providers.dart';
import 'package:shorebird_cli/src/command_runner.dart';
import 'package:shorebird_cli/src/http_client/http_client.dart';
import 'package:shorebird_cli/src/logger.dart';
@@ -20,6 +22,14 @@ import 'package:test/test.dart';
import '../fakes.dart';
import '../mocks.dart';
class FakeProvider extends AuthProvider {
@override
Uri get authorizationEndpoint => Uri.https('example.com');
@override
Uri get tokenEndpoint => Uri.https('example.com');
}
void main() {
group('scoped', () {
test('creates instance with default constructor', () {
@@ -37,6 +47,22 @@ void main() {
});
});
group('OauthValues', () {
final fakeAuthProvider = FakeProvider();
group('clientId', () {
test('throws UnsupportedError when provider is not a known type', () {
expect(() => fakeAuthProvider.clientId, throwsUnsupportedError);
});
});
group('scopes', () {
test('throws UnsupportedError when provider is not a known type', () {
expect(() => fakeAuthProvider.scopes, throwsUnsupportedError);
});
});
});
group('JwtClaims', () {
group('email', () {
test('returns null when idToken is not a valid jwt', () {
@@ -56,6 +82,62 @@ void main() {
});
});
group('OauthAuthProvider', () {
late Jwt jwt;
late JwtPayload payload;
setUp(() {
payload = MockJwtPayload();
jwt = Jwt(
header: MockJwtHeader(),
payload: payload,
signature: 'signature',
);
});
group('authProvider', () {
group('when issuer is login.microsoft.online', () {
setUp(() {
when(() => payload.iss)
.thenReturn('https://login.microsoftonline.com');
});
test('returns AuthProvider.microsoft', () {
expect(jwt.authProvider, isA<MicrosoftAuthProvider>());
});
});
group('when issuer is accounts.google.com', () {
setUp(() {
when(() => payload.iss).thenReturn('https://accounts.google.com');
});
test('returns AuthProvider.google', () {
expect(jwt.authProvider, isA<GoogleAuthProvider>());
});
});
group('when issuer is unknown', () {
setUp(() {
when(() => payload.iss).thenReturn('https://example.com');
});
test('throws exception', () {
expect(
() => jwt.authProvider,
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'message',
'Exception: Unknown jwt issuer: https://example.com',
),
),
);
});
});
});
});
group(Auth, () {
const idToken =
'''eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMjM0NSIsImhkIjoic2hvcmViaXJkLmRldiIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWF0IjoxMjM0LCJleHAiOjY3ODl9.MYbITALvKsGYTYjw1o7AQ0ObkqRWVBSr9cFYJrvA46g''';
@@ -63,6 +145,8 @@ void main() {
const user = User(id: 42, email: email);
const refreshToken = '';
const scopes = <String>[];
final googleAuthProvider = GoogleAuthProvider();
final microsoftAuthProvider = MicrosoftAuthProvider();
final accessToken = AccessToken(
'Bearer',
'accessToken',
@@ -107,7 +191,7 @@ void main() {
return codePushClient;
},
obtainAccessCredentials:
(clientId, scopes, client, userPrompt) async {
(authProvider, clientId, scopes, client, userPrompt) async {
return accessCredentials;
},
),
@@ -135,15 +219,17 @@ void main() {
group('AuthenticatedClient', () {
group('token', () {
const token = 'shorebird-token';
const token =
'''eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMjM0NSIsImhkIjoic2hvcmViaXJkLmRldiIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWF0IjoxMjM0LCJleHAiOjY3ODl9.MYbITALvKsGYTYjw1o7AQ0ObkqRWVBSr9cFYJrvA46g''';
test('does not require an onRefreshCredentials callback', () {
expect(
() => AuthenticatedClient.token(
token: token,
httpClient: httpClient,
refreshCredentials: (clientId, credentials, client) async =>
accessCredentials,
refreshCredentials:
(authProvider, clientId, credentials, client) async =>
accessCredentials,
),
returnsNormally,
);
@@ -164,8 +250,9 @@ void main() {
token: token,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials: (clientId, credentials, client) async =>
accessCredentials,
refreshCredentials:
(authProvider, clientId, credentials, client) async =>
accessCredentials,
);
await runWithOverrides(
@@ -197,8 +284,9 @@ void main() {
token: token,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials: (clientId, credentials, client) async =>
accessCredentials,
refreshCredentials:
(authProvider, clientId, credentials, client) async =>
accessCredentials,
);
await runWithOverrides(
@@ -228,6 +316,8 @@ void main() {
),
);
const expiredIdToken =
'''eyJhbGciOiJIUzI1NiIsImtpZCI6IjEyMzQiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhenAiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJhdWQiOiI1MjMzMDIyMzMyOTMtZWlhNWFudG0wdGd2ZWsyNDB0NDZvcmN0a3RpYWJyZWsuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMjM0NSIsImhkIjoic2hvcmViaXJkLmRldiIsImVtYWlsIjoidGVzdEBlbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaWF0IjoxMjM0LCJleHAiOjY3ODl9.MYbITALvKsGYTYjw1o7AQ0ObkqRWVBSr9cFYJrvA46g''';
final onRefreshCredentialsCalls = <AccessCredentials>[];
final expiredCredentials = AccessCredentials(
AccessToken(
@@ -237,15 +327,16 @@ void main() {
),
'',
[],
idToken: 'expiredIdToken',
idToken: expiredIdToken,
);
final client = AuthenticatedClient.credentials(
credentials: expiredCredentials,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials: (clientId, credentials, client) async =>
accessCredentials,
refreshCredentials:
(authProvider, clientId, credentials, client) async =>
accessCredentials,
);
await runWithOverrides(
@@ -302,7 +393,7 @@ void main() {
HttpStatus.ok,
),
);
await auth.login((_) {});
await auth.login(googleAuthProvider, prompt: (_) {});
final client = auth.client;
expect(client, isA<http.Client>());
expect(client, isA<AuthenticatedClient>());
@@ -347,20 +438,32 @@ void main() {
group('login', () {
test('should set the email when claims are valid and current user exists',
() async {
await auth.login((_) {});
await auth.login(googleAuthProvider, prompt: (_) {});
expect(auth.email, email);
expect(auth.isAuthenticated, isTrue);
expect(buildAuth().email, email);
expect(buildAuth().isAuthenticated, isTrue);
});
group('with a custom auth provider', () {
test(
'''should set the email when claims are valid and current user exists''',
() async {
await auth.login(microsoftAuthProvider, prompt: (_) {});
expect(auth.email, email);
expect(auth.isAuthenticated, isTrue);
expect(buildAuth().email, email);
expect(buildAuth().isAuthenticated, isTrue);
});
});
test('throws UserAlreadyLoggedInException if user is authenticated',
() async {
writeCredentials();
auth = buildAuth();
await expectLater(
auth.login((_) {}),
auth.login(googleAuthProvider, prompt: (_) {}),
throwsA(isA<UserAlreadyLoggedInException>()),
);
@@ -373,7 +476,7 @@ void main() {
.thenAnswer((_) async => null);
await expectLater(
auth.login((_) {}),
auth.login(googleAuthProvider, prompt: (_) {}),
throwsA(isA<UserNotFoundException>()),
);
@@ -395,7 +498,7 @@ void main() {
'returns credentials and does not set the email or cache credentials',
() async {
await expectLater(
auth.loginCI((_) {}),
auth.loginCI(googleAuthProvider, prompt: (_) {}),
completion(equals(accessCredentials)),
);
expect(auth.email, isNull);
@@ -412,7 +515,7 @@ void main() {
).thenAnswer((_) async => null);
await expectLater(
auth.loginCI((_) {}),
auth.loginCI(googleAuthProvider, prompt: (_) {}),
throwsA(isA<UserNotFoundException>()),
);
@@ -422,7 +525,7 @@ void main() {
group('logout', () {
test('clears session and wipes state', () async {
await auth.login((_) {});
await auth.login(googleAuthProvider, prompt: (_) {});
expect(auth.email, email);
expect(auth.isAuthenticated, isTrue);
@@ -0,0 +1,12 @@
import 'package:shorebird_cli/src/auth/providers/providers.dart';
import 'package:test/test.dart';
void main() {
group(MicrosoftAuthProvider, () {
test('has valid endpoints', () {
final provider = MicrosoftAuthProvider();
expect(provider.authorizationEndpoint, isNotNull);
expect(provider.tokenEndpoint, isNotNull);
});
});
}
@@ -1,3 +1,4 @@
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
@@ -28,6 +29,10 @@ void main() {
);
}
setUpAll(() {
registerFallbackValue(GoogleAuthProvider());
});
setUp(() {
auth = MockAuth();
httpClient = MockHttpClient();
@@ -39,7 +44,10 @@ void main() {
test('exits with code 70 if no user is found', () async {
when(
() => auth.loginCI(any()),
() => auth.loginCI(
any(),
prompt: any(named: 'prompt'),
),
).thenThrow(UserNotFoundException(email: email));
final result = await runWithOverrides(command.run);
@@ -55,12 +63,22 @@ void main() {
test('exits with code 70 when error occurs', () async {
final error = Exception('oops something went wrong!');
when(() => auth.loginCI(any())).thenThrow(error);
when(
() => auth.loginCI(
any(),
prompt: any(named: 'prompt'),
),
).thenThrow(error);
final result = await runWithOverrides(command.run);
expect(result, equals(ExitCode.software.code));
verify(() => auth.loginCI(any())).called(1);
verify(
() => auth.loginCI(
any(),
prompt: any(named: 'prompt'),
),
).called(1);
verify(() => logger.err(error.toString())).called(1);
});
@@ -68,13 +86,23 @@ void main() {
const token = 'shorebird-token';
final credentials = MockAccessCredentials();
when(() => credentials.refreshToken).thenReturn(token);
when(() => auth.loginCI(any())).thenAnswer((_) async => credentials);
when(
() => auth.loginCI(
any(),
prompt: any(named: 'prompt'),
),
).thenAnswer((_) async => credentials);
when(() => auth.email).thenReturn(email);
final result = await runWithOverrides(command.run);
expect(result, equals(ExitCode.success.code));
verify(() => auth.loginCI(any())).called(1);
verify(
() => auth.loginCI(
any(),
prompt: any(named: 'prompt'),
),
).called(1);
verify(
() => logger.info(any(that: contains('${lightCyan.wrap(token)}'))),
).called(1);
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
@@ -32,6 +33,10 @@ void main() {
);
}
setUpAll(() {
registerFallbackValue(GoogleAuthProvider());
});
setUp(() {
applicationConfigHome = Directory.systemTemp.createTempSync();
auth = MockAuth();
@@ -48,7 +53,10 @@ void main() {
test('exits with code 0 when already logged in', () async {
when(
() => auth.login(any()),
() => auth.login(
any(),
prompt: any(named: 'prompt'),
),
).thenThrow(UserAlreadyLoggedInException(email: email));
final result = await runWithOverrides(command.run);
@@ -66,7 +74,10 @@ void main() {
test('exits with code 70 if no user is found', () async {
when(
() => auth.login(any()),
() => auth.login(
any(),
prompt: any(named: 'prompt'),
),
).thenThrow(UserNotFoundException(email: email));
final result = await runWithOverrides(command.run);
@@ -82,23 +93,43 @@ void main() {
test('exits with code 70 when error occurs', () async {
final error = Exception('oops something went wrong!');
when(() => auth.login(any())).thenThrow(error);
when(
() => auth.login(
any(),
prompt: any(named: 'prompt'),
),
).thenThrow(error);
final result = await runWithOverrides(command.run);
expect(result, equals(ExitCode.software.code));
verify(() => auth.login(any())).called(1);
verify(
() => auth.login(
any(),
prompt: any(named: 'prompt'),
),
).called(1);
verify(() => logger.err(error.toString())).called(1);
});
test('exits with code 0 when logged in successfully', () async {
when(() => auth.login(any())).thenAnswer((_) async {});
when(
() => auth.login(
any(),
prompt: any(named: 'prompt'),
),
).thenAnswer((_) async {});
when(() => auth.email).thenReturn(email);
final result = await runWithOverrides(command.run);
expect(result, equals(ExitCode.success.code));
verify(() => auth.login(any())).called(1);
verify(
() => auth.login(
any(),
prompt: any(named: 'prompt'),
),
).called(1);
verify(
() => logger.info(
any(that: contains('You are now logged in as <$email>.')),
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:args/args.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:mocktail/mocktail.dart';
import 'package:platform/platform.dart';
@@ -90,6 +91,10 @@ class MockIosArchiveDiffer extends Mock implements IosArchiveDiffer {}
class MockJava extends Mock implements Java {}
class MockJwtHeader extends Mock implements JwtHeader {}
class MockJwtPayload extends Mock implements JwtPayload {}
class MockLogger extends Mock implements Logger {}
class MockOperatingSystemInterface extends Mock
+11 -80
View File
@@ -2,18 +2,12 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'src/adc_utils.dart';
import 'src/auth_http_utils.dart';
import 'src/http_client_base.dart';
import 'src/metadata_server_client.dart' show clientViaMetadataServer;
import 'src/oauth2_flows/authorization_code_grant_manual_flow.dart';
import 'src/oauth2_flows/authorization_code_grant_server_flow.dart';
import 'src/service_account_credentials.dart';
import 'src/typedefs.dart';
export 'googleapis_auth.dart';
export 'src/metadata_server_client.dart';
@@ -22,79 +16,6 @@ export 'src/oauth2_flows/auth_code.dart'
export 'src/service_account_client.dart';
export 'src/typedefs.dart';
/// Create a client using
/// [Application Default Credentials](https://cloud.google.com/docs/authentication/production).
///
/// Looks for credentials in the following order of preference:
/// 1. A JSON file whose path is specified by `GOOGLE_APPLICATION_CREDENTIALS`,
/// this file typically contains [exported service account keys][svc-keys].
/// 2. A JSON file created by
/// [`gcloud auth application-default login`][gcloud-login]
/// in a well-known location (`%APPDATA%/gcloud/application_default_credentials.json`
/// on Windows and `$HOME/.config/gcloud/application_default_credentials.json` on Linux/Mac).
/// 3. On Google Compute Engine and App Engine Flex we fetch credentials from
/// [GCE metadata service][meta-data].
///
/// [meta-data]: https://cloud.google.com/compute/docs/storing-retrieving-metadata
/// [svc-keys]: https://cloud.google.com/docs/authentication/getting-started
/// [gcloud-login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
///
/// {@macro googleapis_auth_baseClient_param}
///
/// {@macro googleapis_auth_returned_auto_refresh_client}
Future<AutoRefreshingAuthClient> clientViaApplicationDefaultCredentials({
required List<String> scopes,
Client? baseClient,
}) async {
if (baseClient == null) {
baseClient = Client();
} else {
baseClient = nonClosingClient(baseClient);
}
// If env var specifies a file to load credentials from we'll do that.
final credsEnv = Platform.environment['GOOGLE_APPLICATION_CREDENTIALS'];
if (credsEnv != null && credsEnv.isNotEmpty) {
// If env var is specific and not empty, we always try to load, even if
// the file doesn't exist.
return await fromApplicationsCredentialsFile(
File(credsEnv),
'GOOGLE_APPLICATION_CREDENTIALS',
scopes,
baseClient,
);
}
// Attempt to use file created by `gcloud auth application-default login`
File credFile;
if (Platform.isWindows) {
credFile = File.fromUri(
Uri.directory(Platform.environment['APPDATA']!)
.resolve('gcloud/application_default_credentials.json'),
);
} else {
final homeVar = Platform.environment['HOME'];
if (homeVar == null) {
throw StateError('The expected environment variable HOME must be set.');
}
credFile = File.fromUri(
Uri.directory(homeVar)
.resolve('.config/gcloud/application_default_credentials.json'),
);
}
// Only try to load from credFile if it exists.
if (await credFile.exists()) {
return await fromApplicationsCredentialsFile(
credFile,
'`gcloud auth application-default login`',
scopes,
baseClient,
);
}
return await clientViaMetadataServer(baseClient: baseClient);
}
/// Obtains oauth2 credentials and returns an authenticated HTTP client.
///
/// See [obtainAccessCredentialsViaUserConsent] for specifics about the
@@ -116,6 +37,7 @@ Future<AutoRefreshingAuthClient> clientViaApplicationDefaultCredentials({
/// {@macro googleapis_auth_not_close_the_baseClient}
/// {@macro googleapis_auth_listen_port}
Future<AutoRefreshingAuthClient> clientViaUserConsent(
AuthProvider authProvider,
ClientId clientId,
List<String> scopes,
PromptUserForConsent userPrompt, {
@@ -130,6 +52,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsent(
}
final flow = AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
baseClient,
@@ -150,6 +73,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsent(
}
return AutoRefreshingClient(
baseClient,
authProvider,
clientId,
credentials,
closeUnderlyingClient: closeUnderlyingClient,
@@ -172,6 +96,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsent(
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
AuthProvider authProvider,
ClientId clientId,
List<String> scopes,
PromptUserForConsentManual userPrompt, {
@@ -185,6 +110,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
}
final flow = AuthorizationCodeGrantManualFlow(
authProvider,
clientId,
scopes,
baseClient,
@@ -205,6 +131,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
return AutoRefreshingClient(
baseClient,
authProvider,
clientId,
credentials,
closeUnderlyingClient: closeUnderlyingClient,
@@ -232,6 +159,7 @@ Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
/// on the Google Cloud console.
/// {@endtemplate}
Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
AuthProvider authProvider,
ClientId clientId,
List<String> scopes,
Client client,
@@ -240,6 +168,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
int listenPort = 0,
}) =>
AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
client,
@@ -261,6 +190,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
///
/// {@macro googleapis_auth_user_consent_return}
Future<AccessCredentials> obtainAccessCredentialsViaUserConsentManual(
AuthProvider authProvider,
ClientId clientId,
List<String> scopes,
Client client,
@@ -268,6 +198,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaUserConsentManual(
String? hostedDomain,
}) =>
AuthorizationCodeGrantManualFlow(
authProvider,
clientId,
scopes,
client,
+1
View File
@@ -27,6 +27,7 @@ library googleapis_auth;
export 'src/auth_client.dart';
export 'src/auth_functions.dart';
export 'src/auth_provider.dart';
export 'src/client_id.dart';
export 'src/exceptions.dart';
export 'src/response_type.dart';
+4 -3
View File
@@ -6,15 +6,14 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'auth_functions.dart';
import 'auth_http_utils.dart';
import 'service_account_client.dart';
import 'service_account_credentials.dart';
Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
File file,
AuthProvider authProvider,
String fileSource,
List<String> scopes,
Client baseClient,
@@ -39,8 +38,10 @@ Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
);
return AutoRefreshingClient(
baseClient,
authProvider,
clientId,
await refreshCredentials(
authProvider,
clientId,
AccessCredentials(
// Hack: Create empty credentials that have expired.
+13 -10
View File
@@ -4,12 +4,10 @@
import 'dart:async';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'access_credentials.dart';
import 'auth_client.dart';
import 'auth_http_utils.dart';
import 'client_id.dart';
import 'http_client_base.dart';
import 'utils.dart';
@@ -82,6 +80,7 @@ AuthClient authenticatedClient(
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
AutoRefreshingAuthClient autoRefreshingClient(
AuthProvider authProvider,
ClientId clientId,
AccessCredentials credentials,
Client baseClient,
@@ -92,7 +91,7 @@ AutoRefreshingAuthClient autoRefreshingClient(
if (credentials.refreshToken == null) {
throw ArgumentError('Refresh token in AccessCredentials was `null`.');
}
return AutoRefreshingClient(baseClient, clientId, credentials);
return AutoRefreshingClient(baseClient, authProvider, clientId, credentials);
}
/// Obtains refreshed [AccessCredentials] for [clientId] and [credentials].
@@ -101,6 +100,7 @@ AutoRefreshingAuthClient autoRefreshingClient(
///
/// {@macro googleapis_auth_client_for_creds}
Future<AccessCredentials> refreshCredentials(
AuthProvider authProvider,
ClientId clientId,
AccessCredentials credentials,
Client client,
@@ -116,12 +116,15 @@ Future<AccessCredentials> refreshCredentials(
}
// 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,
'client_secret': secret,
'refresh_token': refreshToken,
'grant_type': 'refresh_token',
},
authProvider: authProvider,
);
final accessToken = parseAccessToken(jsonMap);
+9 -6
View File
@@ -4,13 +4,9 @@
import 'dart:async';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'access_credentials.dart';
import 'auth_client.dart';
import 'auth_functions.dart';
import 'client_id.dart';
import 'exceptions.dart';
import 'http_client_base.dart';
/// Will close the underlying `http.Client` depending on a constructor argument.
@@ -91,9 +87,11 @@ class AutoRefreshingClient extends AutoRefreshDelegatingClient {
@override
AccessCredentials credentials;
late Client authClient;
final AuthProvider authProvider;
AutoRefreshingClient(
super.client,
this.authProvider,
this.clientId,
this.credentials, {
super.closeUnderlyingClient,
@@ -114,7 +112,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(
authProvider,
clientId,
credentials,
baseClient,
);
notifyAboutNewCredentials(cred);
credentials = cred;
authClient = AuthenticatedClient(
+14
View File
@@ -0,0 +1,14 @@
import 'known_uris.dart';
abstract class AuthProvider {
Uri get authorizationEndpoint;
Uri get tokenEndpoint;
}
class GoogleAuthProvider extends AuthProvider {
@override
Uri get authorizationEndpoint => googleOauth2AuthorizationEndpoint;
@override
Uri get tokenEndpoint => googleOauth2TokenEndpoint;
}
@@ -8,15 +8,13 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import '../access_credentials.dart';
import '../client_id.dart';
import '../exceptions.dart';
import '../known_uris.dart';
import '../utils.dart';
Uri createAuthenticationUri({
required AuthProvider authProvider,
required String redirectUri,
required String clientId,
required Iterable<String> scopes,
@@ -36,7 +34,7 @@ Uri createAuthenticationUri({
if (hostedDomain != null) 'hd': hostedDomain,
if (state != null) 'state': state,
};
return googleOauth2AuthorizationEndpoint.replace(
return authProvider.authorizationEndpoint.replace(
queryParameters: queryValues,
);
}
@@ -106,6 +104,7 @@ String _stripBase64Equals(String value) {
/// to the server. You should use "anti-request forgery state tokens" to guard
/// against "cross site request forgery" attacks.
Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange(
AuthProvider authProvider,
http.Client client,
ClientId clientId,
String code, {
@@ -121,6 +120,7 @@ Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange(
'grant_type': 'authorization_code',
'redirect_uri': redirectUrl,
},
authProvider: authProvider,
);
final accessToken = parseAccessToken(jsonMap);
@@ -2,6 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:googleapis_auth/src/auth_provider.dart';
import 'package:http/http.dart' as http;
import '../access_credentials.dart';
@@ -10,12 +11,14 @@ import 'auth_code.dart';
import 'base_flow.dart';
abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
final AuthProvider authProvider;
final ClientId clientId;
final String? hostedDomain;
final List<String> scopes;
final http.Client _client;
AuthorizationCodeGrantAbstractFlow(
this.authProvider,
this.clientId,
this.scopes,
this._client, {
@@ -25,9 +28,11 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
Future<AccessCredentials> obtainAccessCredentialsUsingCodeImpl(
String code,
String redirectUri, {
required AuthProvider authProvider,
required String codeVerifier,
}) =>
obtainAccessCredentialsViaCodeExchange(
authProvider,
_client,
clientId,
code,
@@ -41,6 +46,7 @@ abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
required String codeVerifier,
}) =>
createAuthenticationUri(
authProvider: authProvider,
redirectUri: redirectUri,
clientId: clientId.identifier,
scopes: scopes,
@@ -24,6 +24,7 @@ class AuthorizationCodeGrantManualFlow
final PromptUserForConsentManual userPrompt;
AuthorizationCodeGrantManualFlow(
super.authProvider,
super.clientId,
super.scopes,
super.client,
@@ -47,6 +48,7 @@ class AuthorizationCodeGrantManualFlow
return obtainAccessCredentialsUsingCodeImpl(
code,
_redirectionUri,
authProvider: authProvider,
codeVerifier: codeVerifier,
);
}
@@ -27,6 +27,7 @@ class AuthorizationCodeGrantServerFlow
final int listenPort;
AuthorizationCodeGrantServerFlow(
super.authProvider,
super.clientId,
super.scopes,
super.client,
@@ -89,6 +90,7 @@ class AuthorizationCodeGrantServerFlow
final credentials = await obtainAccessCredentialsUsingCodeImpl(
code,
redirectionUri,
authProvider: authProvider,
codeVerifier: codeVerifier,
);
+9 -5
View File
@@ -7,13 +7,14 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import '../access_credentials.dart';
import '../../auth_io.dart';
import '../crypto/rsa.dart';
import '../crypto/rsa_sign.dart';
import '../known_uris.dart';
import '../utils.dart';
import 'base_flow.dart';
/// Currently only supports the Google auth provider.
class JwtFlow extends BaseFlow {
// All details are described at:
// https://developers.google.com/accounts/docs/OAuth2ServiceAccount
@@ -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,
},
authProvider: GoogleAuthProvider(),
);
final accessToken = parseAccessToken(response);
return AccessCredentials(accessToken, null, _scopes);
}
+5 -6
View File
@@ -4,13 +4,11 @@
import 'dart:convert';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' show BaseRequest, Client, StreamedResponse;
import 'package:http_parser/http_parser.dart';
import 'access_token.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 +107,9 @@ extension ClientExtensions on Client {
}
Future<Map<String, dynamic>> oauthTokenRequest(
Map<String, String> postValues,
) async {
Map<String, String> postValues, {
required AuthProvider authProvider,
}) async {
final body = Stream<List<int>>.value(
ascii.encode(
postValues.entries
@@ -118,7 +117,7 @@ extension ClientExtensions on Client {
.join('&'),
),
);
final request = RequestImpl('POST', googleOauth2TokenEndpoint, body)
final request = RequestImpl('POST', authProvider.tokenEndpoint, body)
..headers['content-type'] = _contentTypeUrlEncoded;
return requestJson(request, 'Failed to obtain access credentials.');
+4
View File
@@ -4,6 +4,7 @@ library googleapis_auth.adc_test;
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:googleapis_auth/src/adc_utils.dart'
show fromApplicationsCredentialsFile;
import 'package:googleapis_auth/src/known_uris.dart';
@@ -14,6 +15,7 @@ import 'test_utils.dart';
void main() {
test('fromApplicationsCredentialsFile', () async {
final authProvider = GoogleAuthProvider();
final tmp = await Directory.systemTemp.createTemp('googleapis_auth-test');
try {
final credsFile = File.fromUri(tmp.uri.resolve('creds.json'));
@@ -25,6 +27,7 @@ void main() {
}));
final c = await fromApplicationsCredentialsFile(
credsFile,
authProvider,
'test-credentials-file',
[],
mockClient((Request request) async {
@@ -80,6 +83,7 @@ void main() {
}));
final c = await fromApplicationsCredentialsFile(
credsFile,
GoogleAuthProvider(),
'test-credentials-file',
[],
mockClient((Request request) async {
@@ -31,6 +31,7 @@ final _browserFlowRedirectMatcher = predicate<String>((object) {
void main() {
final clientId = ClientId('id', 'secret');
final scopes = ['s1', 's2'];
final authProvider = GoogleAuthProvider();
// Validation + Responses from the authorization server.
@@ -136,6 +137,7 @@ void main() {
test('successful', () async {
final flow = AuthorizationCodeGrantManualFlow(
authProvider,
clientId,
scopes,
mockClient(successFullResponse(manual: true), expectClose: false),
@@ -150,6 +152,7 @@ void main() {
Future.error(TransportException());
final flow = AuthorizationCodeGrantManualFlow(
authProvider,
clientId,
scopes,
mockClient(successFullResponse(manual: true), expectClose: false),
@@ -160,6 +163,7 @@ void main() {
test('transport-exception', () async {
final flow = AuthorizationCodeGrantManualFlow(
authProvider,
clientId,
scopes,
transportFailure,
@@ -170,6 +174,7 @@ void main() {
test('invalid-server-response', () async {
final flow = AuthorizationCodeGrantManualFlow(
authProvider,
clientId,
scopes,
mockClient(invalidResponse, expectClose: false),
@@ -194,6 +199,20 @@ void main() {
}
}
Future<void> 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 +227,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 +271,7 @@ void main() {
test('successful', () async {
final flow = AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
@@ -234,6 +282,7 @@ void main() {
test('transport-exception', () async {
final flow = AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
transportFailure,
@@ -242,8 +291,49 @@ void main() {
await expectLater(flow.run(), throwsA(isTransportException));
});
test('non-GET request', () async {
final flow = AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPromptInvalidHttpVerb),
);
await expectLater(
flow.run,
throwsA(
isA<Exception>().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(
authProvider,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPromptNonMatchingState),
);
await expectLater(
flow.run,
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'message',
'Exception: Invalid response from server (state did not match).',
),
),
);
});
test('invalid-server-response', () async {
final flow = AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
mockClient(invalidResponse, expectClose: false),
@@ -254,6 +344,7 @@ void main() {
test('failed-authentication', () async {
final flow = AuthorizationCodeGrantServerFlow(
authProvider,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
+15 -2
View File
@@ -19,6 +19,8 @@ final _defaultResponse = Response('', 500);
Future<Response> _defaultResponseHandler(Request _) async => _defaultResponse;
void main() {
final authProvider = GoogleAuthProvider();
test('access-token', () {
final expiry = DateTime.now().subtract(const Duration(seconds: 1));
final expiryUtc = expiry.toUtc();
@@ -162,8 +164,12 @@ void main() {
Future<Response>.error(Exception('transport layer exception'));
test('refreshCredentials-successful', () async {
final newCredentials = await refreshCredentials(clientId, credentials,
mockClient(expectAsync1(successfulRefresh), expectClose: false));
final newCredentials = await refreshCredentials(
authProvider,
clientId,
credentials,
mockClient(expectAsync1(successfulRefresh), expectClose: false),
);
final expectedResultUtc = DateTime.now()
.toUtc()
.add(const Duration(seconds: 3600 - maxExpectedTimeDiffInSeconds));
@@ -181,6 +187,7 @@ void main() {
test('refreshCredentials-http-error', () async {
await expectLater(
refreshCredentials(
authProvider,
clientId,
credentials,
mockClient(serverError, expectClose: false),
@@ -198,6 +205,7 @@ void main() {
test('refreshCredentials-error-response', () async {
await expectLater(
refreshCredentials(
authProvider,
clientId,
credentials,
mockClient(refreshErrorResponse, expectClose: false),
@@ -268,6 +276,7 @@ void main() {
test('up-to-date', () async {
final client = autoRefreshingClient(
authProvider,
clientId,
credentials,
mockClient(
@@ -287,6 +296,7 @@ void main() {
expect(
() => autoRefreshingClient(
authProvider,
clientId,
credentials,
mockClient(_defaultResponseHandler, expectClose: false),
@@ -300,6 +310,7 @@ void main() {
AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1', 's2']);
final client = autoRefreshingClient(
authProvider,
clientId,
credentials,
mockClient(expectAsync1((request) {
@@ -320,6 +331,7 @@ void main() {
AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1', 's2']);
final client = autoRefreshingClient(
authProvider,
clientId,
credentials,
mockClient(expectAsync1((request) async {
@@ -344,6 +356,7 @@ void main() {
AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1']);
final client = autoRefreshingClient(
authProvider,
clientId,
credentials,
mockClient(
@@ -0,0 +1,37 @@
import 'package:googleapis_auth/src/service_account_credentials.dart';
import 'package:test/test.dart';
void main() {
group(ServiceAccountCredentials, () {
group('fromJson', () {
test('throws exception if json is not a map', () {
expect(
() => ServiceAccountCredentials.fromJson('[1,2,3]'),
throwsArgumentError,
);
});
test('throws exception if json is not a service account', () {
expect(
() => ServiceAccountCredentials.fromJson({
'type': 'not_service_account',
'client_id': 'client_id',
'private_key': 'private_key',
'client_email': 'client_email',
}),
throwsArgumentError,
);
});
test('throws exception if json is missing fields', () {
expect(
() => ServiceAccountCredentials.fromJson({
'type': 'service_account',
'client_id': 'client_id',
}),
throwsArgumentError,
);
});
});
});
}