Make the oauth2 lib handle OAuth2 401 errors.
BUG=6813
Review URL: https://codereview.chromium.org//11316325
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@15713 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 7322afa..eb7b062 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -8,6 +8,7 @@
import '../../../http/lib/http.dart' as http;
+import 'authorization_exception.dart';
import 'credentials.dart';
import 'expiration_exception.dart';
import 'utils.dart';
@@ -24,9 +25,9 @@
/// authorization server provides ill-formatted responses, or an
/// [ExpirationException] if the credentials are expired and can't be refreshed.
///
-/// Currently this client doesn't attempt to identify errors from the resource
-/// server that are caused by authentication failure. However, it may throw
-/// [AuthorizationException]s for such errors in the future.
+/// 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
@@ -87,8 +88,28 @@
}).chain((_) {
request.headers['authorization'] = "Bearer ${credentials.accessToken}";
return _httpClient.send(request);
+ }).transform((response) {
+ if (response.statusCode != 401 ||
+ !response.headers.containsKey('www-authenticate')) {
+ return response;
+ }
+
+ var authenticate;
+ try {
+ authenticate = parseAuthenticateHeader(
+ response.headers['www-authenticate']);
+ } on FormatException catch (e) {
+ return response;
+ }
+
+ if (authenticate.first != 'bearer') return response;
+
+ var params = authenticate.last;
+ if (!params.containsKey('error')) return response;
+
+ throw new AuthorizationException(
+ params['error'], params['error_description'], params['error_uri']);
});
- // TODO(nweiz): parse 401 errors that are caused by OAuth errors here.
}
/// Explicitly refreshes this client's credentials. Returns this client.
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 583a5bc..59428b3 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -65,6 +65,52 @@
toSplit.substring(index + pattern.length)];
}
+/// A WWW-Authenticate header value, parsed as per [RFC 2617][].
+///
+/// [RFC 2617]: http://tools.ietf.org/html/rfc2617
+class AuthenticateHeader {
+ final String scheme;
+ final Map<String, String> parameters;
+
+ AuthenticateHeader(this.scheme, this.parameters);
+
+ /// Parses a header string. Throws a [FormatException] if the header is
+ /// invalid.
+ factory AuthenticateHeader.parse(String header) {
+ var split = split1(header, ' ');
+ if (split.length == 0) {
+ throw new FormatException('Invalid WWW-Authenticate header: "$header"');
+ } else if (split.length == 1 || split[1].trim().isEmpty) {
+ return new AuthenticateHeader(split[0].toLowerCase(), {});
+ }
+ var scheme = split[0].toLowerCase();
+ var paramString = split[1];
+
+ // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html.
+ var tokenChar = r'[^\0-\x1F()<>@,;:\\"/\[\]?={} \t\x7F]';
+ var quotedStringChar = r'(?:[^\0-\x1F\x7F"]|\\.)';
+ var regexp = new RegExp('^ *($tokenChar+)="($quotedStringChar*)" *(, *)?');
+
+ var parameters = {};
+ var match;
+ do {
+ match = regexp.firstMatch(paramString);
+ if (match == null) {
+ throw new FormatException('Invalid WWW-Authenticate header: "$header"');
+ }
+
+ paramString = paramString.substring(match.end);
+ parameters[match.group(1).toLowerCase()] = match.group(2);
+ } while (match.group(3) != null);
+
+ if (!paramString.trim().isEmpty) {
+ throw new FormatException('Invalid WWW-Authenticate header: "$header"');
+ }
+
+ return new AuthenticateHeader(scheme, parameters);
+ }
+}
+
/// Returns a [Future] that asynchronously completes to `null`.
Future get async {
var completer = new Completer();
diff --git a/test/client_test.dart b/test/client_test.dart
index 94b7920..fba14a6 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -117,4 +117,109 @@
expect(client.refreshCredentials(), throwsStateError);
});
});
+
+ group('with invalid credentials', () {
+ setUp(createHttpClient);
+
+ test('throws an AuthorizationException for a 401 response', () {
+ var credentials = new oauth2.Credentials('access token');
+ var client = new oauth2.Client('identifier', 'secret', credentials,
+ httpClient: httpClient);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(requestUri.toString()));
+ expect(request.headers['authorization'],
+ equals('Bearer access token'));
+
+ var authenticate = 'Bearer error="invalid_token", error_description='
+ '"Something is terribly wrong."';
+ return new Future.immediate(new http.Response('bad job', 401,
+ headers: {'www-authenticate': authenticate}));
+ });
+
+ expect(client.read(requestUri), throwsAuthorizationException);
+ });
+
+ test('passes through a 401 response without www-authenticate', () {
+ var credentials = new oauth2.Credentials('access token');
+ var client = new oauth2.Client('identifier', 'secret', credentials,
+ httpClient: httpClient);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(requestUri.toString()));
+ expect(request.headers['authorization'],
+ equals('Bearer access token'));
+
+ return new Future.immediate(new http.Response('bad job', 401));
+ });
+
+ expect(
+ client.get(requestUri).transform((response) => response.statusCode),
+ completion(equals(401)));
+ });
+
+ test('passes through a 401 response with invalid www-authenticate', () {
+ var credentials = new oauth2.Credentials('access token');
+ var client = new oauth2.Client('identifier', 'secret', credentials,
+ httpClient: httpClient);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(requestUri.toString()));
+ expect(request.headers['authorization'],
+ equals('Bearer access token'));
+
+ var authenticate = 'Bearer error="invalid_token", error_description='
+ '"Something is terribly wrong.", ';
+ return new Future.immediate(new http.Response('bad job', 401,
+ headers: {'www-authenticate': authenticate}));
+ });
+
+ expect(
+ client.get(requestUri).transform((response) => response.statusCode),
+ completion(equals(401)));
+ });
+
+ test('passes through a 401 response with non-bearer www-authenticate', () {
+ var credentials = new oauth2.Credentials('access token');
+ var client = new oauth2.Client('identifier', 'secret', credentials,
+ httpClient: httpClient);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(requestUri.toString()));
+ expect(request.headers['authorization'],
+ equals('Bearer access token'));
+
+ return new Future.immediate(new http.Response('bad job', 401,
+ headers: {'www-authenticate': 'Digest'}));
+ });
+
+ expect(
+ client.get(requestUri).transform((response) => response.statusCode),
+ completion(equals(401)));
+ });
+
+ test('passes through a 401 response with non-OAuth2 www-authenticate', () {
+ var credentials = new oauth2.Credentials('access token');
+ var client = new oauth2.Client('identifier', 'secret', credentials,
+ httpClient: httpClient);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(requestUri.toString()));
+ expect(request.headers['authorization'],
+ equals('Bearer access token'));
+
+ return new Future.immediate(new http.Response('bad job', 401,
+ headers: {'www-authenticate': 'Bearer'}));
+ });
+
+ expect(
+ client.get(requestUri).transform((response) => response.statusCode),
+ completion(equals(401)));
+ });
+ });
}
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..d76e2b0
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,91 @@
+// 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.
+
+library utils_test;
+
+import '../../unittest/lib/unittest.dart';
+import '../lib/src/utils.dart';
+
+
+void main() {
+ group('AuthenticateHeader', () {
+ test("parses a scheme", () {
+ var header = new AuthenticateHeader.parse('bearer');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({}));
+ });
+
+ test("lower-cases the scheme", () {
+ var header = new AuthenticateHeader.parse('BeaRer');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({}));
+ });
+
+ test("parses a scheme with trailing whitespace", () {
+ var header = new AuthenticateHeader.parse('bearer ');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({}));
+ });
+
+ test("parses a scheme with one param", () {
+ var header = new AuthenticateHeader.parse('bearer foo="bar"');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({'foo': 'bar'}));
+ });
+
+ test("parses a scheme with several params", () {
+ var header = new AuthenticateHeader.parse(
+ 'bearer foo="bar", bar="baz" ,baz="qux"');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({
+ 'foo': 'bar',
+ 'bar': 'baz',
+ 'baz': 'qux'
+ }));
+ });
+
+ test("lower-cases parameter names but not values", () {
+ var header = new AuthenticateHeader.parse('bearer FoO="bAr"');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({'foo': 'bAr'}));
+ });
+
+ test("allows empty values", () {
+ var header = new AuthenticateHeader.parse('bearer foo=""');
+ expect(header.scheme, equals('bearer'));
+ expect(header.parameters, equals({'foo': ''}));
+ });
+
+ test("won't parse an empty string", () {
+ expect(() => new AuthenticateHeader.parse(''),
+ throwsFormatException);
+ });
+
+ test("won't parse a token without a value", () {
+ expect(() => new AuthenticateHeader.parse('bearer foo'),
+ throwsFormatException);
+
+ expect(() => new AuthenticateHeader.parse('bearer foo='),
+ throwsFormatException);
+ });
+
+ test("won't parse a token without a value", () {
+ expect(() => new AuthenticateHeader.parse('bearer foo'),
+ throwsFormatException);
+
+ expect(() => new AuthenticateHeader.parse('bearer foo='),
+ throwsFormatException);
+ });
+
+ test("won't parse a trailing comma", () {
+ expect(() => new AuthenticateHeader.parse('bearer foo="bar",'),
+ throwsFormatException);
+ });
+
+ test("won't parse a multiple params without a comma", () {
+ expect(() => new AuthenticateHeader.parse('bearer foo="bar" bar="baz"'),
+ throwsFormatException);
+ });
+ });
+}