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/10 19:05:08 UTC

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

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

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

commit d50d9f94e1faf2f4dd431a5f84f23c8db8e1be57
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    | 169 +++++++++++++++++++++
 .../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, 586 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..02fcb7a3c
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerScheme.java
@@ -0,0 +1,169 @@
+/*
+ * ====================================================================
+ * 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");
+                    final String uri = paramMap.get("error_uri");
+                    if (desc != null || uri != null) {
+                        buf.append(" (");
+                        buf.append(desc).append("; ").append(uri);
+                        buf.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..420475baa
--- /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));
+        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());
+    }
+
+}