blob: fb62eb302287b34303c7205bd73cb6862257713e [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.
library googleapis_auth.auth_code_flow;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../../auth.dart';
import '../http_client_base.dart';
import '../typedefs.dart';
import '../utils.dart';
// The OAuth2 Token endpoint can be used to make requests as
// https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=<token>
//
// A successfull response from the server will give an HTTP response status
// 200 and a body of the following type:
// {
// "issued_to": "XYZ.apps.googleusercontent.com",
// "audience": "XYZ.apps.googleusercontent.com",
// "scope": "https://www.googleapis.com/auth/bigquery",
// "expires_in": 3547,
// "access_type": "offline"
// }
//
// Scopes are separated by spaces.
Future<List<String>> obtainScopesFromAccessToken(
String accessToken, http.Client client) async {
var url = Uri.parse('https://www.googleapis.com/oauth2/v2/tokeninfo'
'?access_token=${Uri.encodeQueryComponent(accessToken)}');
var response = await client.post(url);
if (response.statusCode == 200) {
Map json = jsonDecode(response.body);
var scope = json['scope'];
if (scope is! String) {
throw new Exception(
'The response did not include a `scope` value of type `String`.');
}
return scope.split(' ').toList();
} else {
throw new Exception('Unable to obtain list of scopes an access token '
'is valid for. Server responded with ${response.statusCode}.');
}
}
Future<AccessCredentials> obtainAccessCredentialsUsingCode(
ClientId clientId, String code, String redirectUrl, http.Client client,
[List<String> scopes]) async {
var uri = Uri.parse('https://accounts.google.com/o/oauth2/token');
var formValues = [
'grant_type=authorization_code',
'code=${Uri.encodeQueryComponent(code)}',
'redirect_uri=${Uri.encodeQueryComponent(redirectUrl)}',
'client_id=${Uri.encodeQueryComponent(clientId.identifier)}',
'client_secret=${Uri.encodeQueryComponent(clientId.secret)}',
];
var body = new Stream<List<int>>.fromIterable(
<List<int>>[ascii.encode(formValues.join('&'))]);
var request = new RequestImpl('POST', uri, body);
request.headers['content-type'] = CONTENT_TYPE_URLENCODED;
var response = await client.send(request);
Map jsonMap = await response.stream
.transform(utf8.decoder)
.transform(json.decoder)
.first;
var idToken = jsonMap['id_token'];
var tokenType = jsonMap['token_type'];
var accessToken = jsonMap['access_token'];
var seconds = jsonMap['expires_in'];
var refreshToken = jsonMap['refresh_token'];
var error = jsonMap['error'];
if (response.statusCode != 200 && error != null) {
throw new Exception('Failed to exchange authorization code. '
'Response was ${response.statusCode}. Error message was $error.');
}
if (response.statusCode != 200 ||
accessToken == null ||
seconds is! int ||
tokenType != 'Bearer') {
throw new Exception('Failed to exchange authorization code. '
'Invalid server response. '
'Http status code was: ${response.statusCode}.');
}
if (scopes != null) {
return new AccessCredentials(
new AccessToken('Bearer', accessToken, expiryDate(seconds)),
refreshToken,
scopes,
idToken: idToken);
}
scopes = await obtainScopesFromAccessToken(accessToken, client);
return new AccessCredentials(
new AccessToken('Bearer', accessToken, expiryDate(seconds)),
refreshToken,
scopes,
idToken: idToken);
}
/// Abstract class for obtaining access credentials via the authorization code
/// grant flow
///
/// See
/// * [AuthorizationCodeGrantServerFlow]
/// * [AuthorizationCodeGrantManualFlow]
/// for further details.
abstract class AuthorizationCodeGrantAbstractFlow {
final ClientId clientId;
final List<String> scopes;
final http.Client _client;
AuthorizationCodeGrantAbstractFlow(this.clientId, this.scopes, this._client);
Future<AccessCredentials> run();
Future<AccessCredentials> _obtainAccessCredentialsUsingCode(
String code, String redirectUri) {
return obtainAccessCredentialsUsingCode(
clientId, code, redirectUri, _client, scopes);
}
String _authenticationUri(String redirectUri, {String state}) {
// TODO: Increase scopes with [include_granted_scopes].
var queryValues = [
'response_type=code',
'client_id=${Uri.encodeQueryComponent(clientId.identifier)}',
'redirect_uri=${Uri.encodeQueryComponent(redirectUri)}',
'scope=${Uri.encodeQueryComponent(scopes.join(' '))}',
];
if (state != null) {
queryValues.add('state=${Uri.encodeQueryComponent(state)}');
}
return Uri.parse('https://accounts.google.com/o/oauth2/auth'
'?${queryValues.join('&')}')
.toString();
}
}
/// Runs an oauth2 authorization code grant flow using an HTTP server.
///
/// This class is able to run an oauth2 authorization flow. It takes a user
/// supplied function which will be called with an URI. The user is expected
/// to navigate to that URI and to grant access to the client.
///
/// Once the user has granted access to the client, Google will redirect the
/// user agent to a URL pointing to a locally running HTTP server. Which in turn
/// will be able to extract the authorization code from the URL and use it to
/// obtain access credentials.
class AuthorizationCodeGrantServerFlow
extends AuthorizationCodeGrantAbstractFlow {
final PromptUserForConsent userPrompt;
AuthorizationCodeGrantServerFlow(ClientId clientId, List<String> scopes,
http.Client client, this.userPrompt)
: super(clientId, scopes, client);
Future<AccessCredentials> run() async {
HttpServer server = await HttpServer.bind('localhost', 0);
try {
var port = server.port;
var redirectionUri = 'http://localhost:$port';
var state = 'authcodestate${new DateTime.now().millisecondsSinceEpoch}';
// Prompt user and wait until he goes to URL and the google authorization
// server calls back to our locally running HTTP server.
userPrompt(_authenticationUri(redirectionUri, state: state));
var request = await server.first;
var uri = request.uri;
try {
var returnedState = uri.queryParameters['state'];
var code = uri.queryParameters['code'];
var error = uri.queryParameters['error'];
if (request.method != 'GET') {
throw new Exception('Invalid response from server '
'(expected GET request callback, got: ${request.method}).');
}
if (state != returnedState) {
throw new Exception(
'Invalid response from server (state did not match).');
}
if (error != null) {
throw new UserConsentException(
'Error occured while obtaining access credentials: $error');
}
if (code == null || code == '') {
throw new Exception(
'Invalid response from server (no auth code transmitted).');
}
var credentials =
await _obtainAccessCredentialsUsingCode(code, redirectionUri);
// TODO: We could introduce a user-defined redirect page.
request.response
..statusCode = 200
..headers.set('content-type', 'text/html; charset=UTF-8')
..write('''
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Authorization successful.</title>
</head>
<body>
<h2 style="text-align: center">Application has successfully obtained access credentials</h2>
<p style="text-align: center">This window can be closed now.</p>
</body>
</html>''');
await request.response.close();
return credentials;
} catch (e) {
request.response.statusCode = 500;
await request.response.close().catchError((_) {});
rethrow;
}
} finally {
await server.close();
}
}
}
/// Runs an oauth2 authorization code grant flow using manual Copy&Paste.
///
/// This class is able to run an oauth2 authorization flow. It takes a user
/// supplied function which will be called with an URI. The user is expected
/// to navigate to that URI and to grant access to the client.
///
/// Google will give the resource owner a code. The user supplied function needs
/// to complete with that code.
///
/// The authorization code will then be used to obtain access credentials.
class AuthorizationCodeGrantManualFlow
extends AuthorizationCodeGrantAbstractFlow {
final PromptUserForConsentManual userPrompt;
AuthorizationCodeGrantManualFlow(ClientId clientId, List<String> scopes,
http.Client client, this.userPrompt)
: super(clientId, scopes, client);
Future<AccessCredentials> run() async {
var redirectionUri = 'urn:ietf:wg:oauth:2.0:oob';
// Prompt user and wait until he goes to URL and copy&pastes the auth code
// in.
var code = await userPrompt(_authenticationUri(redirectionUri));
// Use code to obtain credentials
return _obtainAccessCredentialsUsingCode(code, redirectionUri);
}
}
// TODO: Server app flow is missing here.