blob: 92f35481d7a774bed7ce14e8277bd919037f1bfa [file] [log] [blame]
// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
// 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:async';
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:googleapis_auth/src/known_uris.dart';
import 'package:googleapis_auth/src/oauth2_flows/authorization_code_grant_manual_flow.dart';
import 'package:googleapis_auth/src/oauth2_flows/authorization_code_grant_server_flow.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';
import '../test_utils.dart';
typedef RequestHandler = Future<Response> Function(Request _);
final _browserFlowRedirectMatcher = predicate<String>((object) {
if (object.startsWith('redirect_uri=')) {
final url = Uri.parse(
Uri.decodeComponent(object.substring('redirect_uri='.length)));
expect(url.scheme, equals('http'));
expect(url.host, equals('localhost'));
return true;
}
return false;
});
void main() {
final clientId = ClientId('id', 'secret');
final scopes = ['s1', 's2'];
// Validation + Responses from the authorization server.
RequestHandler successFullResponse({required bool manual}) =>
(Request request) async {
expect(request.method, equals('POST'));
expect(request.url, googleOauth2TokenEndpoint);
expect(
request.headers['content-type']!,
startsWith('application/x-www-form-urlencoded'),
);
final pairs = request.body.split('&');
expect(pairs, hasLength(6));
expect(
pairs,
containsAll([
'grant_type=authorization_code',
'code=mycode',
'client_id=id',
'client_secret=secret',
allOf(
startsWith('code_verifier='),
hasLength(142), // happens to be the output length as implemented!
),
if (manual) 'redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob',
if (!manual) _browserFlowRedirectMatcher
]),
);
final result = {
'token_type': 'Bearer',
'access_token': 'tokendata',
'expires_in': 3600,
'refresh_token': 'my-refresh-token',
'id_token': 'my-id-token',
'scope': 's1 s2',
};
return Response(
jsonEncode(result),
200,
headers: jsonContentType,
);
};
Future<Response> invalidResponse(Request request) async {
// Missing expires_in field!
final result = {
'token_type': 'Bearer',
'access_token': 'tokendata',
'refresh_token': 'my-refresh-token',
'id_token': 'my-id-token',
};
return Response(jsonEncode(result), 200, headers: jsonContentType);
}
// Validation functions for user prompt and access credentials.
void validateAccessCredentials(AccessCredentials credentials) {
expect(credentials.accessToken.data, equals('tokendata'));
expect(credentials.accessToken.type, equals('Bearer'));
expect(credentials.scopes, equals(['s1', 's2']));
expect(credentials.refreshToken, equals('my-refresh-token'));
expect(credentials.idToken, equals('my-id-token'));
expectExpiryOneHourFromNow(credentials.accessToken);
}
Uri validateUserPromptUri(String url, {bool manual = false}) {
final uri = Uri.parse(url);
expect(uri.scheme, googleOauth2AuthorizationEndpoint.scheme);
expect(uri.authority, googleOauth2AuthorizationEndpoint.authority);
expect(uri.path, googleOauth2AuthorizationEndpoint.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<String> manualUserPrompt(String url) async {
validateUserPromptUri(url, manual: true);
return 'mycode';
}
test('successful', () async {
final flow = AuthorizationCodeGrantManualFlow(
clientId,
scopes,
mockClient(successFullResponse(manual: true), expectClose: false),
manualUserPrompt,
);
validateAccessCredentials(await flow.run());
});
test('user-exception', () async {
// We use a TransportException here for convenience.
Future<String> manualUserPromptError(String url) =>
Future.error(TransportException());
final flow = AuthorizationCodeGrantManualFlow(
clientId,
scopes,
mockClient(successFullResponse(manual: true), expectClose: false),
manualUserPromptError,
);
await expectLater(flow.run(), throwsA(isTransportException));
});
test('transport-exception', () async {
final flow = AuthorizationCodeGrantManualFlow(
clientId,
scopes,
transportFailure,
manualUserPrompt,
);
await expectLater(flow.run(), throwsA(isTransportException));
});
test('invalid-server-response', () async {
final flow = AuthorizationCodeGrantManualFlow(
clientId,
scopes,
mockClient(invalidResponse, expectClose: false),
manualUserPrompt,
);
await expectLater(flow.run(), throwsA(isServerRequestFailedException));
});
});
group('http-server', () {
Future<void> callRedirectionEndpoint(Uri authCodeCall) async {
final ioClient = HttpClient();
final closeMe = expectAsync0(ioClient.close);
try {
final request = await ioClient.getUrl(authCodeCall);
final response = await request.close();
await response.drain();
} finally {
closeMe();
}
}
void userPrompt(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',
});
callRedirectionEndpoint(authCodeCall);
}
void userPromptInvalidAuthCodeCallback(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'],
'error': 'failed to authenticate',
});
callRedirectionEndpoint(authCodeCall);
}
test('successful', () async {
final flow = AuthorizationCodeGrantServerFlow(
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPrompt),
);
validateAccessCredentials(await flow.run());
});
test('transport-exception', () async {
final flow = AuthorizationCodeGrantServerFlow(
clientId,
scopes,
transportFailure,
expectAsync1(userPrompt),
);
await expectLater(flow.run(), throwsA(isTransportException));
});
test('invalid-server-response', () async {
final flow = AuthorizationCodeGrantServerFlow(
clientId,
scopes,
mockClient(invalidResponse, expectClose: false),
expectAsync1(userPrompt),
);
await expectLater(flow.run(), throwsA(isServerRequestFailedException));
});
test('failed-authentication', () async {
final flow = AuthorizationCodeGrantServerFlow(
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPromptInvalidAuthCodeCallback),
);
await expectLater(flow.run(), throwsA(isUserConsentException));
});
}, testOn: '!browser');
});
}