blob: a476700143aac6e6811361b4da39334161817893 [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.implicit_gapi_flow;
import "dart:async";
import 'dart:html' as html;
import "dart:js" as js;
import '../../auth.dart';
import '../utils.dart';
// This will be overridden by tests.
String gapiUrl = 'https://apis.google.com/js/client.js';
/// This class performs the implicit browser-based oauth2 flow.
///
/// It has to be used in two steps:
///
/// 1. First call initialize() and wait until the Future completes successfully
/// - loads the 'gapi' JavaScript library into the current document
/// - wait until the library signals it is ready
///
/// 2. Call login() as often as needed.
/// - will call the 'gapi' JavaScript lib to trigger an oauth2 browser flow
/// => This might create a popup which asks the user for consent.
/// - will wait until the flow is completed (successfully or not)
/// => Completes with AccessToken or an Exception.
/// 3. Call loginHybrid() as often as needed.
/// - will call the 'gapi' JavaScript lib to trigger an oauth2 browser flow
/// => This might create a popup which asks the user for consent.
/// - will wait until the flow is completed (successfully or not)
/// => Completes with a tuple [AccessCredentials cred, String authCode]
/// or an Exception.
class ImplicitFlow {
static const CallbackTimeout = const Duration(seconds: 20);
final String _clientId;
final List<String> _scopes;
ImplicitFlow(this._clientId, this._scopes);
Future initialize() {
var completer = new Completer();
var timeout = new Timer(CallbackTimeout, () {
completer.completeError(new Exception(
'Timed out while waiting for the gapi.auth library to load.'));
});
js.context['dartGapiLoaded'] = () {
timeout.cancel();
try {
var gapi = js.context['gapi']['auth'];
try {
gapi.callMethod('init', [
() {
completer.complete();
}
]);
} on NoSuchMethodError {
throw new StateError('gapi.auth not loaded.');
}
} catch (error, stack) {
completer.completeError(error, stack);
}
};
var script = new html.ScriptElement();
script.src = '${gapiUrl}?onload=dartGapiLoaded';
script.onError.first.then((errorEvent) {
timeout.cancel();
if (!completer.isCompleted) {
// script loading errors can still happen after timeouts
completer.completeError(new Exception('Failed to load gapi library.'));
}
});
html.document.body.append(script);
return completer.future;
}
Future<LoginResult> loginHybrid(
{bool force: false, bool immediate: false, String loginHint}) =>
_login(force, immediate, true, loginHint);
Future<AccessCredentials> login(
{bool force: false, bool immediate: false, String loginHint}) async {
return (await _login(force, immediate, false, loginHint)).credential;
}
// Completes with either credentials or a tuple of credentials and authCode.
// hybrid => [AccessCredentials credentials, String authCode]
// !hybrid => AccessCredentials
Future<LoginResult> _login(
bool force, bool immediate, bool hybrid, String loginHint) {
var completer = new Completer<LoginResult>();
var gapi = js.context['gapi']['auth'];
var json = {
'client_id': _clientId,
'immediate': immediate,
'approval_prompt': force ? 'force' : 'auto',
'response_type': hybrid ? 'code token' : 'token',
'scope': _scopes.join(' '),
'access_type': hybrid ? 'offline' : 'online',
};
if (loginHint != null) {
json['login_hint'] = loginHint;
}
gapi.callMethod('authorize', [
new js.JsObject.jsify(json),
(jsTokenObject) {
var tokenType = jsTokenObject['token_type'];
var token = jsTokenObject['access_token'];
var expiresInRaw = jsTokenObject['expires_in'];
var code = jsTokenObject['code'];
var error = jsTokenObject['error'];
var expiresIn;
if (expiresInRaw is String) {
expiresIn = int.parse(expiresInRaw);
}
if (error != null) {
completer.completeError(
new UserConsentException('Failed to get user consent: $error.'));
} else if (token == null ||
expiresIn is! int ||
tokenType != 'Bearer') {
completer.completeError(new Exception(
'Failed to obtain user consent. Invalid server response.'));
} else {
var accessToken =
new AccessToken('Bearer', token, expiryDate(expiresIn));
var credentials = new AccessCredentials(accessToken, null, _scopes);
if (hybrid) {
if (code == null) {
completer.completeError(new Exception('Expected to get auth code '
'from server in hybrid flow, but did not.'));
}
completer.complete(new LoginResult(credentials, code: code));
} else {
completer.complete(new LoginResult(credentials));
}
}
}
]);
return completer.future;
}
}
class LoginResult {
final AccessCredentials credential;
final String code;
LoginResult(this.credential, {this.code});
}