Skip to content

Add credentials for bearer token authentication #60048

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions sdk/lib/_http/http.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2065,6 +2065,13 @@ abstract final class HttpClientBasicCredentials
_HttpClientBasicCredentials(username, password);
}

/// Represents credentials for bearer token authentication.
abstract final class HttpClientBearerCredentials
implements HttpClientCredentials {
factory HttpClientBearerCredentials(String token) =>
_HttpClientBearerCredentials(token);
}

/// Represents credentials for digest authentication. Digest
/// authentication is only supported for servers using the MD5
/// algorithm and quality of protection (qop) of either "none" or
Expand Down
32 changes: 31 additions & 1 deletion sdk/lib/_http/http_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3900,18 +3900,21 @@ class _AuthenticationScheme {

static const UNKNOWN = _AuthenticationScheme(-1);
static const BASIC = _AuthenticationScheme(0);
static const DIGEST = _AuthenticationScheme(1);
static const BEARER = _AuthenticationScheme(1);
static const DIGEST = _AuthenticationScheme(2);

const _AuthenticationScheme(this._scheme);

factory _AuthenticationScheme.fromString(String scheme) {
if (scheme.toLowerCase() == "basic") return BASIC;
if (scheme.toLowerCase() == "bearer") return BEARER;
if (scheme.toLowerCase() == "digest") return DIGEST;
return UNKNOWN;
}

String toString() {
if (this == BASIC) return "Basic";
if (this == BEARER) return "Bearer";
if (this == DIGEST) return "Digest";
return "Unknown";
}
Expand Down Expand Up @@ -4038,6 +4041,33 @@ final class _HttpClientBasicCredentials extends _HttpClientCredentials
}
}

final class _HttpClientBearerCredentials extends _HttpClientCredentials
implements HttpClientBearerCredentials {
String token;

_HttpClientBearerCredentials(this.token) {
// verify token according to RFC-6750 section 2.1
// https://www.rfc-editor.org/rfc/rfc6750.html#section-2.1
if (RegExp(r'[^0-9A-Za-z\-._~+/=]').hasMatch(token)) {
throw ArgumentError.value(token, "token", "Invalid characters");
}
}

_AuthenticationScheme get scheme => _AuthenticationScheme.BEARER;

String authorization() {
return "Bearer $token";
}

void authorize(_Credentials _, HttpClientRequest request) {
request.headers.set(HttpHeaders.authorizationHeader, authorization());
}

void authorizeProxy(_ProxyCredentials _, HttpClientRequest request) {
request.headers.set(HttpHeaders.proxyAuthorizationHeader, authorization());
}
}

final class _HttpClientDigestCredentials extends _HttpClientCredentials
implements HttpClientDigestCredentials {
String username;
Expand Down
1 change: 1 addition & 0 deletions sdk/lib/io/io.dart
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export 'dart:_http'
HttpClientResponseCompressionState,
HttpClientCredentials,
HttpClientBasicCredentials,
HttpClientBearerCredentials,
HttpClientDigestCredentials,
HttpConnectionInfo,
RedirectInfo,
Expand Down
243 changes: 243 additions & 0 deletions tests/standalone/io/http_auth_bearer_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// Copyright (c) 2025, 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:expect/async_helper.dart";
import "package:expect/expect.dart";

class Server {
late HttpServer server;

Future<Server> start() async {
server = await HttpServer.bind(InternetAddress.loopbackIPv4.address, 0);
server.listen((request) {
final response = request.response;

// WARNING: this authenticate header is malformed because of missing
// commas between the arguments
if (request.uri.path == "/malformedAuthenticate") {
response.statusCode = HttpStatus.unauthorized;
response.headers.set(HttpHeaders.wwwAuthenticateHeader,
"Bearer realm=\"realm\" error=\"invalid_token\"");
response.close();
return;
}

// NOTE: see RFC6750 section 3 regarding the authenticate response header
// field
// https://www.rfc-editor.org/rfc/rfc6750.html#section-3
if (request.headers[HttpHeaders.authorizationHeader] != null) {
final token = base64.encode(utf8.encode(request.uri.path.substring(1)));
Expect.equals(
1, request.headers[HttpHeaders.authorizationHeader]!.length);
final authorizationHeaderParts =
request.headers[HttpHeaders.authorizationHeader]![0].split(" ");
Expect.equals("Bearer", authorizationHeaderParts[0]);
if (token != authorizationHeaderParts[1]) {
response.statusCode = HttpStatus.unauthorized;
response.headers.set(HttpHeaders.wwwAuthenticateHeader,
"Bearer realm=\"realm\", error=\"invalid_token\"");
}
} else {
response.statusCode = HttpStatus.unauthorized;
response.headers
.set(HttpHeaders.wwwAuthenticateHeader, "Bearer realm=\"realm\"");
}
response.close();
});
return this;
}

void shutdown() {
server.close();
}

String get host => server.address.address;

int get port => server.port;
}

void testValidBearerTokens() {
HttpClientBearerCredentials("977ce44bc56dc5000c9d2c329e682547");
HttpClientBearerCredentials("dGVzdHRlc3R0ZXN0dGVzdA==");
HttpClientBearerCredentials("mF_9.B5f-4.1JqM");
}

void testInvalidBearerTokens() {
Expect.throws(() => HttpClientBearerCredentials("#(&%)"));
Expect.throws(() => HttpClientBearerCredentials("áéîöü"));
Expect.throws(() => HttpClientBearerCredentials("あいうえお"));
Expect.throws(() => HttpClientBearerCredentials(" "));
}

void testBearerWithoutCredentials() async {
final server = await Server().start();
final client = HttpClient();

Future makeRequest(Uri url) async {
final request = await client.getUrl(url);
final response = await request.close();
Expect.equals(HttpStatus.unauthorized, response.statusCode);
return response.drain();
}

await Future.wait([
for (int i = 0; i < 5; i++) ...[
makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
],
]);

server.shutdown();
client.close();
}

void testBearerWithCredentials() async {
final server = await Server().start();
final client = HttpClient();

Future makeRequest(Uri url) async {
final request = await client.getUrl(url);
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
return response.drain();
}

for (int i = 0; i < 5; i++) {
final token = base64.encode(utf8.encode("test$i"));
client.addCredentials(
Uri.parse("http://${server.host}:${server.port}/test$i"),
"realm",
HttpClientBearerCredentials(token));
}

await Future.wait([
for (int i = 0; i < 5; i++) ...[
makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
],
]);

server.shutdown();
client.close();
}

void testBearerWithAuthenticateCallback() async {
final server = await Server().start();
final client = HttpClient();

final callbacks = <String>{};

client.authenticate = (url, scheme, realm) async {
Expect.equals("Bearer", scheme);
Expect.equals("realm", realm);
callbacks.add(url.path.substring(1));
String token = base64.encode(utf8.encode(url.path.substring(1)));
await Future.delayed(const Duration(milliseconds: 10));
client.addCredentials(url, realm!, HttpClientBearerCredentials(token));
return true;
};

Future makeRequest(Uri url) async {
final request = await client.getUrl(url);
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
return response.drain();
}

await Future.wait([
for (int i = 0; i < 5; i++) ...[
makeRequest(Uri.parse("http://${server.host}:${server.port}/test$i")),
],
]);

// assert that all authenticate callbacks have actually been called
Expect.setEquals({for (int i = 0; i < 5; i++) "test$i"}, callbacks);

server.shutdown();
client.close();
}

void testMalformedAuthenticateHeaderWithoutCredentials() async {
final server = await Server().start();
final client = HttpClient();
final uri =
Uri.parse("http://${server.host}:${server.port}/malformedAuthenticate");

// the request should resolve normally if no authentication is configured
final request = await client.getUrl(uri);
final response = await request.close();

server.shutdown();
client.close();
}

void testMalformedAuthenticateHeaderWithCredentials() async {
final server = await Server().start();
final client = HttpClient();
final uri =
Uri.parse("http://${server.host}:${server.port}/malformedAuthenticate");
final token = base64.encode(utf8.encode("test"));

// the request should throw an exception if credentials have been added
client.addCredentials(uri, "realm", HttpClientBearerCredentials(token));
await asyncExpectThrows<HttpException>(Future(() async {
final request = await client.getUrl(uri);
final response = await request.close();
}));

server.shutdown();
client.close();
}

void testMalformedAuthenticateHeaderWithAuthenticateCallback() async {
final server = await Server().start();
final client = HttpClient();
final uri =
Uri.parse("http://${server.host}:${server.port}/malformedAuthenticate");

// the request should throw an exception if the authenticate handler is set
client.authenticate = (url, scheme, realm) async => false;
await asyncExpectThrows<HttpException>(Future(() async {
final request = await client.getUrl(uri);
final response = await request.close();
}));

server.shutdown();
client.close();
}

void testLocalServerBearer() async {
final client = HttpClient();

client.authenticate = (url, scheme, realm) async {
final token = base64.encode(utf8.encode("test"));
client.addCredentials(Uri.parse("http://127.0.0.1/bearer"), "test",
HttpClientBearerCredentials(token));
return true;
};

final request =
await client.getUrl(Uri.parse("http://127.0.0.1/bearer/test"));
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
await response.drain();

client.close();
}

main() {
testValidBearerTokens();
testInvalidBearerTokens();
testBearerWithoutCredentials();
testBearerWithCredentials();
testBearerWithAuthenticateCallback();
testMalformedAuthenticateHeaderWithoutCredentials();
testMalformedAuthenticateHeaderWithCredentials();
testMalformedAuthenticateHeaderWithAuthenticateCallback();
// These tests are not normally run. They can be used for locally
// testing with another web server (e.g. Apache).
//testLocalServerBearer();
}
24 changes: 22 additions & 2 deletions tests/standalone/io/http_auth_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ void testMalformedAuthenticateHeaderWithAuthHandler() {
// Request should throw an exception if the authenticate handler is set
client.authenticate = (url, scheme, realm) async => false;
await asyncExpectThrows<HttpException>(
client.getUrl(uri).then((request) => request.close()));
client.getUrl(uri).then((request) => request.close()));

server.shutdown();
client.close();
Expand All @@ -252,7 +252,7 @@ void testMalformedAuthenticateHeaderWithCredentials() {
client.addCredentials(
uri, 'realm', HttpClientBasicCredentials('dart', 'password'));
await asyncExpectThrows<HttpException>(
client.getUrl(uri).then((request) => request.close()));
client.getUrl(uri).then((request) => request.close()));

server.shutdown();
client.close();
Expand All @@ -279,6 +279,25 @@ void testLocalServerBasic() {
});
}

void testLocalServerBearer() async {
final client = HttpClient();

client.authenticate = (url, scheme, realm) async {
final token = base64.encode(utf8.encode("test"));
client.addCredentials(Uri.parse("http://127.0.0.1/bearer"), "test",
HttpClientBearerCredentials(token));
return true;
};

final request =
await client.getUrl(Uri.parse("http://127.0.0.1/bearer/test"));
final response = await request.close();
Expect.equals(HttpStatus.ok, response.statusCode);
await response.drain();

client.close();
}

void testLocalServerDigest() {
HttpClient client = new HttpClient();

Expand Down Expand Up @@ -311,5 +330,6 @@ main() {
// These teste are not normally run. They can be used for locally
// testing with another web server (e.g. Apache).
//testLocalServerBasic();
//testLocalServerBearer();
//testLocalServerDigest();
}