blob: 367d5838bec1f85604fdeb45d162a5839d5b3f06 [file] [log] [blame]
// Copyright (c) 2012, 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 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import 'authorization_exception.dart';
import 'credentials.dart';
import 'expiration_exception.dart';
// TODO(nweiz): Add an onCredentialsRefreshed event once we have some event
// infrastructure.
/// An OAuth2 client.
///
/// This acts as a drop-in replacement for an [http.Client], while sending
/// OAuth2 authorization credentials along with each request.
///
/// The client also automatically refreshes its credentials if possible. When it
/// makes a request, if its credentials are expired, it will first refresh them.
/// This means that any request may throw an [AuthorizationException] if the
/// refresh is not authorized for some reason, a [FormatException] if the
/// authorization server provides ill-formatted responses, or an
/// [ExpirationException] if the credentials are expired and can't be refreshed.
///
/// The client will also throw an [AuthorizationException] if the resource
/// server returns a 401 response with a WWW-Authenticate header indicating that
/// the current credentials are invalid.
///
/// If you already have a set of [Credentials], you can construct a [Client]
/// directly. However, in order to first obtain the credentials, you must
/// authorize. At the time of writing, the only authorization method this
/// library supports is [AuthorizationCodeGrant].
class Client extends http.BaseClient {
/// The client identifier for this client.
///
/// The authorization server will issue each client a separate client
/// identifier and secret, which allows the server to tell which client is
/// accessing it. Some servers may also have an anonymous identifier/secret
/// pair that any client may use.
///
/// This is usually global to the program using this library.
final String identifier;
/// The client secret for this client.
///
/// The authorization server will issue each client a separate client
/// identifier and secret, which allows the server to tell which client is
/// accessing it. Some servers may also have an anonymous identifier/secret
/// pair that any client may use.
///
/// This is usually global to the program using this library.
///
/// Note that clients whose source code or binary executable is readily
/// available may not be able to make sure the client secret is kept a secret.
/// This is fine; OAuth2 servers generally won't rely on knowing with
/// certainty that a client is who it claims to be.
final String secret;
/// The credentials this client uses to prove to the resource server that it's
/// authorized.
///
/// This may change from request to request as the credentials expire and the
/// client refreshes them automatically.
Credentials get credentials => _credentials;
Credentials _credentials;
/// Whether to use HTTP Basic authentication for authorizing the client.
final bool _basicAuth;
/// The underlying HTTP client.
http.Client _httpClient;
/// Creates a new client from a pre-existing set of credentials.
///
/// When authorizing a client for the first time, you should use
/// [AuthorizationCodeGrant] or [resourceOwnerPasswordGrant] instead of
/// constructing a [Client] directly.
///
/// [httpClient] is the underlying client that this forwards requests to after
/// adding authorization credentials to them.
///
/// Throws an [ArgumentError] if [secret] is passed without [identifier].
Client(this._credentials,
{this.identifier,
this.secret,
bool basicAuth = true,
http.Client httpClient})
: _basicAuth = basicAuth,
_httpClient = httpClient == null ? new http.Client() : httpClient {
if (identifier == null && secret != null) {
throw new ArgumentError("secret may not be passed without identifier.");
}
}
/// Sends an HTTP request with OAuth2 authorization credentials attached.
///
/// This will also automatically refresh this client's [Credentials] before
/// sending the request if necessary.
Future<http.StreamedResponse> send(http.BaseRequest request) async {
if (credentials.isExpired) {
if (!credentials.canRefresh) throw new ExpirationException(credentials);
await refreshCredentials();
}
request.headers['authorization'] = "Bearer ${credentials.accessToken}";
var response = await _httpClient.send(request);
if (response.statusCode != 401) return response;
if (!response.headers.containsKey('www-authenticate')) return response;
var challenges;
try {
challenges = AuthenticationChallenge.parseHeader(
response.headers['www-authenticate']);
} on FormatException {
return response;
}
var challenge = challenges.firstWhere(
(challenge) => challenge.scheme == 'bearer',
orElse: () => null);
if (challenge == null) return response;
var params = challenge.parameters;
if (!params.containsKey('error')) return response;
throw new AuthorizationException(
params['error'],
params['error_description'],
params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
}
/// Explicitly refreshes this client's credentials. Returns this client.
///
/// This will throw a [StateError] if the [Credentials] can't be refreshed, an
/// [AuthorizationException] if refreshing the credentials fails, or a
/// [FormatError] if the authorization server returns invalid responses.
///
/// You may request different scopes than the default by passing in
/// [newScopes]. These must be a subset of the scopes in the
/// [Credentials.scopes] field of [Client.credentials].
Future<Client> refreshCredentials([List<String> newScopes]) async {
if (!credentials.canRefresh) {
var prefix = "OAuth credentials";
if (credentials.isExpired) prefix = "$prefix have expired and";
throw new StateError("$prefix can't be refreshed.");
}
_credentials = await credentials.refresh(
identifier: identifier,
secret: secret,
newScopes: newScopes,
basicAuth: _basicAuth,
httpClient: _httpClient);
return this;
}
/// Closes this client and its underlying HTTP client.
void close() {
if (_httpClient != null) _httpClient.close();
_httpClient = null;
}
}