You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2023/01/07 11:18:14 UTC

[httpcomponents-client] branch bearer_auth_support updated (a4fd5af17 -> 4d5ab954b)

This is an automated email from the ASF dual-hosted git repository.

olegk pushed a change to branch bearer_auth_support
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git


    omit a4fd5af17 BEARER auth scheme support (RFC 6750)
     new 4d5ab954b BEARER auth scheme support (RFC 6750)

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (a4fd5af17)
            \
             N -- N -- N   refs/heads/bearer_auth_support (4d5ab954b)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../src/main/java/org/apache/hc/client5/http/auth/BearerToken.java   | 5 -----
 .../main/java/org/apache/hc/client5/http/impl/auth/BearerScheme.java | 2 +-
 .../test/java/org/apache/hc/client5/http/auth/TestCredentials.java   | 1 -
 .../java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java  | 1 -
 4 files changed, 1 insertion(+), 8 deletions(-)


[httpcomponents-client] 01/01: BEARER auth scheme support (RFC 6750)

Posted by ol...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

olegk pushed a commit to branch bearer_auth_support
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git

commit 4d5ab954bc69e59990d0880e72351abfa49d4fa2
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Tue Dec 6 21:05:56 2022 +0100

    BEARER auth scheme support (RFC 6750)
---
 .../testing/auth/BearerAuthenticationHandler.java} |  25 +---
 .../hc/client5/testing/BasicTestAuthenticator.java |  20 +++
 .../AbstractHttpAsyncClientAuthenticationTest.java |  65 ++++++++
 .../testing/sync/TestClientAuthentication.java     |  70 ++++++++-
 .../hc/client5/http/impl/win/WinHttpClients.java   |   2 +
 .../apache/hc/client5/http/auth/BearerToken.java   |  90 +++++++++++
 .../hc/client5/http/auth/StandardAuthScheme.java   |   7 +-
 .../http/impl/DefaultAuthenticationStrategy.java   |   1 +
 .../http/impl/async/H2AsyncClientBuilder.java      |   2 +
 .../http/impl/async/HttpAsyncClientBuilder.java    |   2 +
 .../hc/client5/http/impl/auth/BearerScheme.java    | 166 +++++++++++++++++++++
 .../http/impl/auth/BearerSchemeFactory.java        |  37 ++---
 .../http/impl/classic/HttpClientBuilder.java       |   2 +
 .../hc/client5/http/auth/TestCredentials.java      |  28 ++++
 .../client5/http/impl/auth/TestBearerScheme.java   | 104 +++++++++++++
 15 files changed, 583 insertions(+), 38 deletions(-)

diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BearerAuthenticationHandler.java
similarity index 62%
copy from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
copy to httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BearerAuthenticationHandler.java
index aeac33fa3..0f2362fd7 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BearerAuthenticationHandler.java
@@ -25,31 +25,20 @@
  *
  */
 
-package org.apache.hc.client5.testing;
+package org.apache.hc.client5.testing.auth;
 
-import java.util.Objects;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
 
-import org.apache.hc.client5.testing.auth.Authenticator;
-import org.apache.hc.core5.net.URIAuthority;
-
-public class BasicTestAuthenticator implements Authenticator {
-
-    private final String userToken;
-    private final String realm;
-
-    public BasicTestAuthenticator(final String userToken, final String realm) {
-        this.userToken = userToken;
-        this.realm = realm;
-    }
+public class BearerAuthenticationHandler extends AbstractAuthenticationHandler {
 
     @Override
-    public boolean authenticate(final URIAuthority authority, final String requestUri, final String credentials) {
-        return Objects.equals(userToken, credentials);
+    String getSchemeName() {
+        return StandardAuthScheme.BEARER;
     }
 
     @Override
-    public String getRealm(final URIAuthority authority, final String requestUri) {
-        return realm;
+    String decodeChallenge(final String challenge) {
+        return challenge;
     }
 
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
index aeac33fa3..eb7f3831a 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
@@ -29,8 +29,11 @@ package org.apache.hc.client5.testing;
 
 import java.util.Objects;
 
+import org.apache.hc.client5.testing.auth.AuthResult;
 import org.apache.hc.client5.testing.auth.Authenticator;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
 import org.apache.hc.core5.net.URIAuthority;
+import org.apache.hc.core5.util.TextUtils;
 
 public class BasicTestAuthenticator implements Authenticator {
 
@@ -47,6 +50,23 @@ public class BasicTestAuthenticator implements Authenticator {
         return Objects.equals(userToken, credentials);
     }
 
+    @Override
+    public AuthResult perform(final URIAuthority authority,
+                              final String requestUri,
+                              final String credentials) {
+        final boolean result = authenticate(authority, requestUri, credentials);
+        if (result) {
+            return new AuthResult(true);
+        } else {
+            if (TextUtils.isBlank(credentials)) {
+                return new AuthResult(false);
+            } else {
+                final String error = credentials.endsWith("-expired") ? "token expired"  : "invalid token";
+                return new AuthResult(false, new BasicNameValuePair("error", error));
+            }
+        }
+    }
+
     @Override
     public String getRealm(final URIAuthority authority, final String requestUri) {
         return realm;
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java
index 3dba15041..f9867fde5 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/async/AbstractHttpAsyncClientAuthenticationTest.java
@@ -28,6 +28,7 @@ package org.apache.hc.client5.testing.async;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Queue;
@@ -45,6 +46,7 @@ import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
 import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.BearerToken;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
@@ -57,6 +59,7 @@ import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.BasicTestAuthenticator;
 import org.apache.hc.client5.testing.auth.Authenticator;
+import org.apache.hc.client5.testing.auth.BearerAuthenticationHandler;
 import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHeaders;
@@ -475,4 +478,66 @@ public abstract class AbstractHttpAsyncClientAuthenticationTest<T extends Closea
                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
     }
 
+    private final static String CHARS = "0123456789abcdef";
+
+    @Test
+    public void testBearerTokenAuthentication() throws Exception {
+        final SecureRandom secureRandom = SecureRandom.getInstanceStrong();
+        secureRandom.setSeed(System.currentTimeMillis());
+        final StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < 16; i++) {
+            buf.append(CHARS.charAt(secureRandom.nextInt(CHARS.length() - 1)));
+        }
+        final String token = buf.toString();
+        final H2TestServer server = startServer(requestHandler ->
+                new AuthenticatingAsyncDecorator(
+                        requestHandler,
+                        new BearerAuthenticationHandler(),
+                        new BasicTestAuthenticator(token, "test realm")));
+        server.register("*", AsyncEchoHandler::new);
+        final HttpHost target = targetHost();
+
+        final T client = startClient();
+
+        final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
+        final HttpClientContext context1 = HttpClientContext.create();
+        context1.setCredentialsProvider(credsProvider);
+
+        final Future<SimpleHttpResponse> future1 = client.execute(SimpleRequestBuilder.get()
+                .setHttpHost(target)
+                .setPath("/")
+                .build(), context1, null);
+        final SimpleHttpResponse response1 = future1.get();
+        Assertions.assertNotNull(response1);
+        Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response1.getCode());
+        Mockito.verify(credsProvider).getCredentials(
+                Mockito.eq(new AuthScope(target, "test realm", "bearer")), Mockito.any());
+
+        final HttpClientContext context2 = HttpClientContext.create();
+        Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
+                .thenReturn(new BearerToken(token));
+        context2.setCredentialsProvider(credsProvider);
+
+        final Future<SimpleHttpResponse> future2 = client.execute(SimpleRequestBuilder.get()
+                .setHttpHost(target)
+                .setPath("/")
+                .build(), context2, null);
+        final SimpleHttpResponse response2 = future2.get();
+        Assertions.assertNotNull(response2);
+        Assertions.assertEquals(HttpStatus.SC_OK, response2.getCode());
+
+        final HttpClientContext context3 = HttpClientContext.create();
+        Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
+                .thenReturn(new BearerToken(token + "-expired"));
+        context3.setCredentialsProvider(credsProvider);
+
+        final Future<SimpleHttpResponse> future3 = client.execute(SimpleRequestBuilder.get()
+                .setHttpHost(target)
+                .setPath("/")
+                .build(), context3, null);
+        final SimpleHttpResponse response3 = future3.get();
+        Assertions.assertNotNull(response3);
+        Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response3.getCode());
+    }
+
 }
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
index 441d934ee..27f2a3f03 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
+++ b/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/sync/TestClientAuthentication.java
@@ -31,6 +31,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Queue;
@@ -44,6 +45,7 @@ import org.apache.hc.client5.http.auth.AuthCache;
 import org.apache.hc.client5.http.auth.AuthScheme;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.BearerToken;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
@@ -61,6 +63,7 @@ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
 import org.apache.hc.client5.testing.BasicTestAuthenticator;
 import org.apache.hc.client5.testing.auth.Authenticator;
+import org.apache.hc.client5.testing.auth.BearerAuthenticationHandler;
 import org.apache.hc.client5.testing.classic.AuthenticatingDecorator;
 import org.apache.hc.client5.testing.classic.EchoHandler;
 import org.apache.hc.client5.testing.sync.extension.TestClientResources;
@@ -424,7 +427,7 @@ public class TestClientAuthentication {
     }
 
     @Test
-    public void testAuthenticationCredentialsCachingReauthenticationOnDifferentRealm() throws Exception {
+    public void testAuthenticationCredentialsCachingReAuthenticationOnDifferentRealm() throws Exception {
         final ClassicTestServer server = startServer(new Authenticator() {
 
             @Override
@@ -762,4 +765,69 @@ public class TestClientAuthentication {
                 Mockito.eq(new AuthScope(target, "test realm", "basic")), Mockito.any());
     }
 
+    private final static String CHARS = "0123456789abcdef";
+
+    @Test
+    public void testBearerTokenAuthentication() throws Exception {
+        final SecureRandom secureRandom = SecureRandom.getInstanceStrong();
+        secureRandom.setSeed(System.currentTimeMillis());
+        final StringBuilder buf = new StringBuilder();
+        for (int i = 0; i < 16; i++) {
+            buf.append(CHARS.charAt(secureRandom.nextInt(CHARS.length() - 1)));
+        }
+        final String token = buf.toString();
+        final ClassicTestServer server = testResources.startServer(
+                Http1Config.DEFAULT,
+                HttpProcessors.server(),
+                requestHandler -> new AuthenticatingDecorator(
+                        requestHandler,
+                        new BearerAuthenticationHandler(),
+                        new BasicTestAuthenticator(token, "test realm")));
+        server.registerHandler("*", new EchoHandler());
+        final HttpHost target = targetHost();
+
+        final CloseableHttpClient client = startClient();
+
+        final CredentialsProvider credsProvider = Mockito.mock(CredentialsProvider.class);
+
+        final HttpClientContext context1 = HttpClientContext.create();
+        context1.setCredentialsProvider(credsProvider);
+        final HttpGet httpget1 = new HttpGet("/");
+        client.execute(target, httpget1, context1, response -> {
+            final HttpEntity entity = response.getEntity();
+            Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+            Assertions.assertNotNull(entity);
+            EntityUtils.consume(entity);
+            return null;
+        });
+        Mockito.verify(credsProvider).getCredentials(
+                Mockito.eq(new AuthScope(target, "test realm", "bearer")), Mockito.any());
+
+        final HttpClientContext context2 = HttpClientContext.create();
+        Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
+                .thenReturn(new BearerToken(token));
+        context2.setCredentialsProvider(credsProvider);
+        final HttpGet httpget2 = new HttpGet("/");
+        client.execute(target, httpget2, context2, response -> {
+            final HttpEntity entity = response.getEntity();
+            Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+            Assertions.assertNotNull(entity);
+            EntityUtils.consume(entity);
+            return null;
+        });
+
+        final HttpClientContext context3 = HttpClientContext.create();
+        Mockito.when(credsProvider.getCredentials(Mockito.any(), Mockito.any()))
+                .thenReturn(new BearerToken(token + "-expired"));
+        context3.setCredentialsProvider(credsProvider);
+        final HttpGet httpget3 = new HttpGet("/");
+        client.execute(target, httpget3, context3, response -> {
+            final HttpEntity entity = response.getEntity();
+            Assertions.assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getCode());
+            Assertions.assertNotNull(entity);
+            EntityUtils.consume(entity);
+            return null;
+        });
+    }
+
 }
diff --git a/httpclient5-win/src/main/java/org/apache/hc/client5/http/impl/win/WinHttpClients.java b/httpclient5-win/src/main/java/org/apache/hc/client5/http/impl/win/WinHttpClients.java
index ef4fca281..c36511740 100644
--- a/httpclient5-win/src/main/java/org/apache/hc/client5/http/impl/win/WinHttpClients.java
+++ b/httpclient5-win/src/main/java/org/apache/hc/client5/http/impl/win/WinHttpClients.java
@@ -31,6 +31,7 @@ import java.util.Locale;
 import org.apache.hc.client5.http.auth.AuthSchemeFactory;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
 import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
@@ -60,6 +61,7 @@ public class WinHttpClients {
             final Registry<AuthSchemeFactory> authSchemeRegistry = RegistryBuilder.<AuthSchemeFactory>create()
                     .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
+                    .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.NTLM, WindowsNTLMSchemeFactory.DEFAULT)
                     .register(StandardAuthScheme.SPNEGO, WindowsNegotiateSchemeFactory.DEFAULT)
                     .build();
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/BearerToken.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/BearerToken.java
new file mode 100644
index 000000000..f4a533161
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/BearerToken.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.Objects;
+
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.util.Args;
+
+/**
+ * Opaque token {@link Credentials} usually representing a set of claims, often encrypted
+ * or signed. The JWT (JSON Web Token) is among most widely used tokens used at the time
+ * of writing.
+ *
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public class BearerToken implements Credentials, Serializable {
+
+    private final String token;
+
+    public BearerToken(final String token) {
+        super();
+        this.token = Args.notBlank(token, "Token");
+    }
+
+    @Override
+    public Principal getUserPrincipal() {
+        return null;
+    }
+
+    /**
+     * @deprecated Do not use.
+     */
+    @Deprecated
+    @Override
+    public char[] getPassword() {
+        return null;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    @Override
+    public int hashCode() {
+        return token.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o instanceof BearerToken) {
+            final BearerToken that = (BearerToken) o;
+            return Objects.equals(this.token, that.token);
+        }
+        return false;
+    }
+
+}
+
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
index 51371cc7b..b5994a91c 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
@@ -39,7 +39,7 @@ public final class StandardAuthScheme {
     }
 
     /**
-     * Basic authentication scheme (considered inherently insecure without transport encryption,
+     * Basic authentication scheme (considered inherently insecure without TLS,
      * but most widely supported).
      */
     public static final String BASIC = "Basic";
@@ -49,6 +49,11 @@ public final class StandardAuthScheme {
      */
     public static final String DIGEST = "Digest";
 
+    /**
+     * Bearer authentication scheme (should be used with TLS).
+     */
+    public static final String BEARER = "Bearer";
+
     /**
      * The NTLM authentication scheme is a proprietary Microsoft Windows
      * authentication protocol as defined in [MS-NLMP].
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
index a12b137ff..64559c4ff 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/DefaultAuthenticationStrategy.java
@@ -68,6 +68,7 @@ public class DefaultAuthenticationStrategy implements AuthenticationStrategy {
                 StandardAuthScheme.SPNEGO,
                 StandardAuthScheme.KERBEROS,
                 StandardAuthScheme.NTLM,
+                StandardAuthScheme.BEARER,
                 StandardAuthScheme.DIGEST,
                 StandardAuthScheme.BASIC));
 
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
index 3cdc556ee..5c594c7ad 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java
@@ -58,6 +58,7 @@ import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
 import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.KerberosSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.NTLMSchemeFactory;
@@ -820,6 +821,7 @@ public class H2AsyncClientBuilder {
             authSchemeRegistryCopy = RegistryBuilder.<AuthSchemeFactory>create()
                     .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
+                    .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.NTLM, NTLMSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.SPNEGO, SPNegoSchemeFactory.DEFAULT)
                     .register(StandardAuthScheme.KERBEROS, KerberosSchemeFactory.DEFAULT)
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
index 09d0657d2..1012284e1 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java
@@ -64,6 +64,7 @@ import org.apache.hc.client5.http.impl.IdleConnectionEvictor;
 import org.apache.hc.client5.http.impl.NoopUserTokenHandler;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.KerberosSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.NTLMSchemeFactory;
@@ -990,6 +991,7 @@ public class HttpAsyncClientBuilder {
             authSchemeRegistryCopy = RegistryBuilder.<AuthSchemeFactory>create()
                     .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
+                    .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.NTLM, NTLMSchemeFactory.INSTANCE)
                     .register(StandardAuthScheme.SPNEGO, SPNegoSchemeFactory.DEFAULT)
                     .register(StandardAuthScheme.KERBEROS, KerberosSchemeFactory.DEFAULT)
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerScheme.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerScheme.java
new file mode 100644
index 000000000..7a8086a14
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerScheme.java
@@ -0,0 +1,166 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.auth;
+
+import java.io.Serializable;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.hc.client5.http.auth.AuthChallenge;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.AuthStateCacheable;
+import org.apache.hc.client5.http.auth.AuthenticationException;
+import org.apache.hc.client5.http.auth.BearerToken;
+import org.apache.hc.client5.http.auth.Credentials;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.client5.http.auth.MalformedChallengeException;
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.Asserts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Bearer authentication scheme.
+ *
+ * @since 5.3
+ */
+@AuthStateCacheable
+public class BearerScheme implements AuthScheme, Serializable {
+
+    private static final Logger LOG = LoggerFactory.getLogger(BearerScheme.class);
+
+    private final Map<String, String> paramMap;
+    private boolean complete;
+
+    private BearerToken bearerToken;
+
+    public BearerScheme() {
+        this.paramMap = new HashMap<>();
+        this.complete = false;
+    }
+
+    @Override
+    public String getName() {
+        return StandardAuthScheme.BEARER;
+    }
+
+    @Override
+    public boolean isConnectionBased() {
+        return false;
+    }
+
+    @Override
+    public String getRealm() {
+        return this.paramMap.get("realm");
+    }
+
+    @Override
+    public void processChallenge(
+            final AuthChallenge authChallenge,
+            final HttpContext context) throws MalformedChallengeException {
+        this.paramMap.clear();
+        final List<NameValuePair> params = authChallenge.getParams();
+        if (params != null) {
+            for (final NameValuePair param: params) {
+                this.paramMap.put(param.getName().toLowerCase(Locale.ROOT), param.getValue());
+            }
+            if (LOG.isDebugEnabled()) {
+                final String error = paramMap.get("error");
+                if (error != null) {
+                    final StringBuilder buf = new StringBuilder();
+                    buf.append(error);
+                    final String desc = paramMap.get("error_description");
+                    if (desc != null) {
+                        buf.append(" (").append(desc).append(")");
+                    }
+                    LOG.debug(buf.toString());
+                }
+            }
+        }
+        this.complete = true;
+    }
+
+    @Override
+    public boolean isChallengeComplete() {
+        return this.complete;
+    }
+
+    @Override
+    public boolean isResponseReady(
+            final HttpHost host,
+            final CredentialsProvider credentialsProvider,
+            final HttpContext context) throws AuthenticationException {
+
+        Args.notNull(host, "Auth host");
+        Args.notNull(credentialsProvider, "Credentials provider");
+
+        final AuthScope authScope = new AuthScope(host, getRealm(), getName());
+        final Credentials credentials = credentialsProvider.getCredentials(authScope, context);
+        if (credentials instanceof BearerToken) {
+            this.bearerToken = (BearerToken) credentials;
+            return true;
+        }
+
+        if (LOG.isDebugEnabled()) {
+            final HttpClientContext clientContext = HttpClientContext.adapt(context);
+            final String exchangeId = clientContext.getExchangeId();
+            LOG.debug("{} No credentials found for auth scope [{}]", exchangeId, authScope);
+        }
+        this.bearerToken = null;
+        return false;
+    }
+
+    @Override
+    public Principal getPrincipal() {
+        return null;
+    }
+
+    @Override
+    public String generateAuthResponse(
+            final HttpHost host,
+            final HttpRequest request,
+            final HttpContext context) throws AuthenticationException {
+        Asserts.notNull(bearerToken, "Bearer token");
+        return StandardAuthScheme.BEARER + " " + bearerToken.getToken();
+    }
+
+    @Override
+    public String toString() {
+        return getName() + this.paramMap;
+    }
+
+}
diff --git a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
similarity index 60%
copy from httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
copy to httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
index aeac33fa3..f05f1be14 100644
--- a/httpclient5-testing/src/test/java/org/apache/hc/client5/testing/BasicTestAuthenticator.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
@@ -25,31 +25,32 @@
  *
  */
 
-package org.apache.hc.client5.testing;
+package org.apache.hc.client5.http.impl.auth;
 
-import java.util.Objects;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthSchemeFactory;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.protocol.HttpContext;
 
-import org.apache.hc.client5.testing.auth.Authenticator;
-import org.apache.hc.core5.net.URIAuthority;
-
-public class BasicTestAuthenticator implements Authenticator {
-
-    private final String userToken;
-    private final String realm;
+/**
+ * {@link AuthSchemeFactory} implementation that creates and initializes
+ * {@link BearerScheme} instances.
+ *
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+public class BearerSchemeFactory implements AuthSchemeFactory {
 
-    public BasicTestAuthenticator(final String userToken, final String realm) {
-        this.userToken = userToken;
-        this.realm = realm;
-    }
+    public static final BearerSchemeFactory INSTANCE = new BearerSchemeFactory();
 
-    @Override
-    public boolean authenticate(final URIAuthority authority, final String requestUri, final String credentials) {
-        return Objects.equals(userToken, credentials);
+    public BearerSchemeFactory() {
+        super();
     }
 
     @Override
-    public String getRealm(final URIAuthority authority, final String requestUri) {
-        return realm;
+    public AuthScheme create(final HttpContext context) {
+        return new BearerScheme();
     }
 
 }
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java
index b8d0d1037..96c67d96a 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java
@@ -65,6 +65,7 @@ import org.apache.hc.client5.http.impl.IdleConnectionEvictor;
 import org.apache.hc.client5.http.impl.NoopUserTokenHandler;
 import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
 import org.apache.hc.client5.http.impl.auth.BasicSchemeFactory;
+import org.apache.hc.client5.http.impl.auth.BearerSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.DigestSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.KerberosSchemeFactory;
 import org.apache.hc.client5.http.impl.auth.NTLMSchemeFactory;
@@ -945,6 +946,7 @@ public class HttpClientBuilder {
             authSchemeRegistryCopy = RegistryBuilder.<AuthSchemeFactory>create()
                 .register(StandardAuthScheme.BASIC, BasicSchemeFactory.INSTANCE)
                 .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE)
+                .register(StandardAuthScheme.BEARER, BearerSchemeFactory.INSTANCE)
                 .register(StandardAuthScheme.NTLM, NTLMSchemeFactory.INSTANCE)
                 .register(StandardAuthScheme.SPNEGO, SPNegoSchemeFactory.DEFAULT)
                 .register(StandardAuthScheme.KERBEROS, KerberosSchemeFactory.DEFAULT)
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/auth/TestCredentials.java b/httpclient5/src/test/java/org/apache/hc/client5/http/auth/TestCredentials.java
index de14d7c48..5b72aa445 100644
--- a/httpclient5/src/test/java/org/apache/hc/client5/http/auth/TestCredentials.java
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/auth/TestCredentials.java
@@ -103,6 +103,34 @@ public class TestCredentials {
         Assertions.assertEquals(creds1, creds3);
     }
 
+    @Test
+    public void tesBearerTokenBasics() {
+        final BearerToken creds1 = new BearerToken("token of some sort");
+        Assertions.assertEquals("token of some sort", creds1.getToken());
+    }
+
+    @Test
+    public void testBearerTokenHashCode() {
+        final BearerToken creds1 = new BearerToken("token of some sort");
+        final BearerToken creds2 = new BearerToken("another token of some sort");
+        final BearerToken creds3 = new BearerToken("token of some sort");
+
+        Assertions.assertTrue(creds1.hashCode() == creds1.hashCode());
+        Assertions.assertTrue(creds1.hashCode() != creds2.hashCode());
+        Assertions.assertTrue(creds1.hashCode() == creds3.hashCode());
+    }
+
+    @Test
+    public void testBearerTokenEquals() {
+        final BearerToken creds1 = new BearerToken("token of some sort");
+        final BearerToken creds2 = new BearerToken("another token of some sort");
+        final BearerToken creds3 = new BearerToken("token of some sort");
+
+        Assertions.assertEquals(creds1, creds1);
+        Assertions.assertNotEquals(creds1, creds2);
+        Assertions.assertEquals(creds1, creds3);
+    }
+
     @Test
     public void testNTCredentialsHashCode() {
         final NTCredentials creds1 = new NTCredentials(
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java
new file mode 100644
index 000000000..ebb428242
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.auth;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.hc.client5.http.auth.AuthChallenge;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.BearerToken;
+import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.CredentialsProvider;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Bearer authentication test cases.
+ */
+public class TestBearerScheme {
+
+    @Test
+    public void testBearerAuthenticationEmptyChallenge() throws Exception {
+        final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "BEARER");
+        final AuthScheme authscheme = new BearerScheme();
+        authscheme.processChallenge(authChallenge, null);
+        Assertions.assertNull(authscheme.getRealm());
+    }
+
+    @Test
+    public void testBearerAuthentication() throws Exception {
+        final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "Bearer",
+                new BasicNameValuePair("realm", "test"));
+
+        final AuthScheme authscheme = new BearerScheme();
+        authscheme.processChallenge(authChallenge, null);
+
+        final HttpHost host  = new HttpHost("somehost", 80);
+        final CredentialsProvider credentialsProvider = CredentialsProviderBuilder.create()
+                .add(new AuthScope(host, "test", null), new BearerToken("some token"))
+                .build();
+
+        final HttpRequest request = new BasicHttpRequest("GET", "/");
+        Assertions.assertTrue(authscheme.isResponseReady(host, credentialsProvider, null));
+        final String authResponse = authscheme.generateAuthResponse(host, request, null);
+
+        Assertions.assertEquals("test", authscheme.getRealm());
+        Assertions.assertTrue(authscheme.isChallengeComplete());
+        Assertions.assertFalse(authscheme.isConnectionBased());
+    }
+
+    @Test
+    public void testSerialization() throws Exception {
+        final AuthChallenge authChallenge = new AuthChallenge(ChallengeType.TARGET, "Bearer",
+                new BasicNameValuePair("realm", "test"),
+                new BasicNameValuePair("code", "read"));
+
+        final AuthScheme authscheme = new BearerScheme();
+        authscheme.processChallenge(authChallenge, null);
+
+        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        final ObjectOutputStream out = new ObjectOutputStream(buffer);
+        out.writeObject(authscheme);
+        out.flush();
+        final byte[] raw = buffer.toByteArray();
+        final ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(raw));
+        final BearerScheme authcheme2 = (BearerScheme) in.readObject();
+
+        Assertions.assertEquals(authcheme2.getName(), authcheme2.getName());
+        Assertions.assertEquals(authcheme2.getRealm(), authcheme2.getRealm());
+        Assertions.assertEquals(authcheme2.isChallengeComplete(), authcheme2.isChallengeComplete());
+    }
+
+}