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 2022/12/01 13:21:28 UTC

[httpcomponents-client] branch bearer_auth_support updated (448c36022 -> a26766d2f)

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


    from 448c36022 Normalize scheme name in AuthScope
     new f8a67ad22 Made authenticating decorators capable of supporting different authentication schemes
     new a26766d2f BEARER auth scheme support (RFC 6750)

The 2 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:
 .../async/AuthenticatingAsyncDecorator.java        |  28 ++++--
 .../auth/AbstractAuthenticationHandler.java        |  77 +++++++++++++++
 ...thenticator.java => AuthenticationHandler.java} |  14 ++-
 .../testing/auth/BasicAuthTokenExtractor.java      |   6 +-
 ...icator.java => BasicAuthenticationHandler.java} |  19 +++-
 ....java => TokenBearerAuthenticationHandler.java} |  14 ++-
 .../testing/classic/AuthenticatingDecorator.java   |  28 ++++--
 .../AbstractHttpAsyncClientAuthenticationTest.java |  66 +++++++++++++
 .../testing/sync/TestClientAuthentication.java     |  72 ++++++++++++++
 .../hc/client5/http/impl/win/WinHttpClients.java   |   2 +
 ...mePasswordCredentials.java => BearerToken.java} |  54 +++++-----
 .../hc/client5/http/auth/StandardAuthScheme.java   |   7 +-
 .../http/impl/DefaultAuthenticationStrategy.java   |   1 +
 .../http/impl/async/H2AsyncClientBuilder.java      |   2 +
 .../http/impl/async/HttpAsyncClientBuilder.java    |   2 +
 .../auth/{BasicScheme.java => BearerScheme.java}   | 110 +++------------------
 ...SchemeFactory.java => BearerSchemeFactory.java} |  18 ++--
 .../http/impl/classic/HttpClientBuilder.java       |   2 +
 .../hc/client5/http/auth/TestCredentials.java      |  32 ++++++
 .../client5/http/impl/auth/TestBearerScheme.java   | 106 ++++++++++++++++++++
 20 files changed, 498 insertions(+), 162 deletions(-)
 create mode 100644 httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java
 copy httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/{Authenticator.java => AuthenticationHandler.java} (80%)
 copy httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/{Authenticator.java => BasicAuthenticationHandler.java} (66%)
 copy httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/{Authenticator.java => TokenBearerAuthenticationHandler.java} (77%)
 copy httpclient5/src/main/java/org/apache/hc/client5/http/auth/{UsernamePasswordCredentials.java => BearerToken.java} (67%)
 copy httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/{BasicScheme.java => BearerScheme.java} (53%)
 copy httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/{NTLMSchemeFactory.java => BearerSchemeFactory.java} (83%)
 create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java


[httpcomponents-client] 01/02: Made authenticating decorators capable of supporting different authentication schemes

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 f8a67ad22c94adf94b2e77cf2212abc63aca9cec
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Nov 27 15:04:24 2022 +0100

    Made authenticating decorators capable of supporting different authentication schemes
---
 .../async/AuthenticatingAsyncDecorator.java        | 28 +++++---
 .../auth/AbstractAuthenticationHandler.java        | 77 ++++++++++++++++++++++
 ...enExtractor.java => AuthenticationHandler.java} | 34 +++-------
 .../testing/auth/BasicAuthTokenExtractor.java      |  6 +-
 ...ractor.java => BasicAuthenticationHandler.java} | 35 ++++------
 .../testing/classic/AuthenticatingDecorator.java   | 28 ++++++--
 6 files changed, 144 insertions(+), 64 deletions(-)

diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java
index cad07b812..f0bff871d 100644
--- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java
@@ -28,12 +28,13 @@ package org.apache.hc.client5.testing.async;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
-import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.testing.auth.AuthenticationHandler;
 import org.apache.hc.client5.testing.auth.Authenticator;
-import org.apache.hc.client5.testing.auth.BasicAuthTokenExtractor;
+import org.apache.hc.client5.testing.auth.BasicAuthenticationHandler;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.EntityDetails;
 import org.apache.hc.core5.http.Header;
@@ -44,6 +45,7 @@ import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.HttpStatus;
 import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
 import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
 import org.apache.hc.core5.http.nio.AsyncResponseProducer;
 import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
 import org.apache.hc.core5.http.nio.CapacityChannel;
@@ -58,15 +60,24 @@ import org.apache.hc.core5.util.Args;
 public class AuthenticatingAsyncDecorator implements AsyncServerExchangeHandler {
 
     private final AsyncServerExchangeHandler exchangeHandler;
+    private final AuthenticationHandler<String> authenticationHandler;
     private final Authenticator authenticator;
     private final AtomicReference<AsyncResponseProducer> responseProducerRef;
-    private final BasicAuthTokenExtractor authTokenExtractor;
 
-    public AuthenticatingAsyncDecorator(final AsyncServerExchangeHandler exchangeHandler, final Authenticator authenticator) {
+    /**
+     * @since 5.3
+     */
+    public AuthenticatingAsyncDecorator(final AsyncServerExchangeHandler exchangeHandler,
+                                        final AuthenticationHandler<String> authenticationHandler,
+                                        final Authenticator authenticator) {
         this.exchangeHandler = Args.notNull(exchangeHandler, "Request handler");
+        this.authenticationHandler = Args.notNull(authenticationHandler, "Authentication handler");
         this.authenticator = Args.notNull(authenticator, "Authenticator");
         this.responseProducerRef = new AtomicReference<>();
-        this.authTokenExtractor = new BasicAuthTokenExtractor();
+    }
+
+    public AuthenticatingAsyncDecorator(final AsyncServerExchangeHandler exchangeHandler, final Authenticator authenticator) {
+        this(exchangeHandler, new BasicAuthenticationHandler(), authenticator);
     }
 
     protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) {
@@ -79,7 +90,7 @@ public class AuthenticatingAsyncDecorator implements AsyncServerExchangeHandler
             final ResponseChannel responseChannel,
             final HttpContext context) throws HttpException, IOException {
         final Header h = request.getFirstHeader(HttpHeaders.AUTHORIZATION);
-        final String challengeResponse = h != null ? authTokenExtractor.extract(h.getValue()) : null;
+        final String challengeResponse = h != null ? authenticationHandler.extractAuthToken(h.getValue()) : null;
 
         final URIAuthority authority = request.getAuthority();
         final String requestUri = request.getRequestUri();
@@ -96,8 +107,9 @@ public class AuthenticatingAsyncDecorator implements AsyncServerExchangeHandler
         } else {
             final HttpResponse unauthorized = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED);
             final String realm = authenticator.getRealm(authority, requestUri);
-            unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"" + realm + "\"");
-
+            final String challenge = authenticationHandler.challenge(
+                    realm != null ? Collections.singletonList(new BasicNameValuePair("realm", realm)) : null);
+            unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, challenge);
             customizeUnauthorizedResponse(unauthorized);
 
             final AsyncResponseProducer responseProducer = new BasicResponseProducer(
diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java
new file mode 100644
index 000000000..4ebeb23fd
--- /dev/null
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java
@@ -0,0 +1,77 @@
+/*
+ * ====================================================================
+ * 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.testing.auth;
+
+import java.util.List;
+
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.ProtocolException;
+
+abstract class AbstractAuthenticationHandler implements AuthenticationHandler<String> {
+
+    abstract String getSchemeName();
+
+    @Override
+    public final String challenge(final List<NameValuePair> params) {
+        final StringBuilder buf = new StringBuilder();
+        buf.append(getSchemeName());
+        if (params != null && params.size() > 0) {
+            buf.append(" ");
+            for (int i = 0; i < params.size(); i++) {
+                if (i > 0) {
+                    buf.append("; ");
+                }
+                final NameValuePair param = params.get(i);
+                buf.append(param.getName()).append("=\"").append(param.getValue()).append("\"");
+            }
+        }
+        return buf.toString();
+    }
+
+    abstract String decodeChallenge(String challenge) throws IllegalArgumentException;
+
+    public final String extractAuthToken(final String challengeResponse) throws HttpException {
+        final int i = challengeResponse.indexOf(' ');
+        if (i == -1) {
+            throw new ProtocolException("Invalid " + getSchemeName() + " challenge response");
+        }
+        final String schemeName = challengeResponse.substring(0, i);
+        if (schemeName.equalsIgnoreCase(getSchemeName())) {
+            final String s = challengeResponse.substring(i + 1).trim();
+            try {
+                return decodeChallenge(s);
+            } catch (final IllegalArgumentException ex) {
+                throw new ProtocolException("Malformed " + getSchemeName() + " credentials");
+            }
+        } else {
+            throw new ProtocolException("Unexpected challenge type");
+        }
+    }
+
+}
diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java
similarity index 50%
copy from httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
copy to httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java
index b07dc9ccf..a19d1e037 100644
--- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java
@@ -27,34 +27,18 @@
 
 package org.apache.hc.client5.testing.auth;
 
-import java.nio.charset.StandardCharsets;
+import java.util.List;
 
-import org.apache.hc.client5.http.utils.Base64;
-import org.apache.hc.client5.http.auth.StandardAuthScheme;
 import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.NameValuePair;
 
-public class BasicAuthTokenExtractor {
+/**
+ * @since 5.3
+ */
+public interface AuthenticationHandler<T> {
+
+    String challenge(List<NameValuePair> params);
 
-    public String extract(final String challengeResponse) throws HttpException {
-        if (challengeResponse != null) {
-            final int i = challengeResponse.indexOf(' ');
-            if (i == -1) {
-                throw new ProtocolException("Invalid challenge response: " + challengeResponse);
-            }
-            final String schemeName = challengeResponse.substring(0, i);
-            if (schemeName.equalsIgnoreCase(StandardAuthScheme.BASIC)) {
-                final String s = challengeResponse.substring(i + 1).trim();
-                try {
-                    final byte[] credsRaw = s.getBytes(StandardCharsets.US_ASCII);
-                    final Base64 codec = new Base64();
-                    return new String(codec.decode(credsRaw), StandardCharsets.US_ASCII);
-                } catch (final IllegalArgumentException ex) {
-                    throw new ProtocolException("Malformed Basic credentials");
-                }
-            }
-        }
-        return null;
-    }
+    T extractAuthToken(String header) throws HttpException;
 
 }
diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
index b07dc9ccf..1ea4ab3c5 100644
--- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
@@ -29,11 +29,15 @@ package org.apache.hc.client5.testing.auth;
 
 import java.nio.charset.StandardCharsets;
 
-import org.apache.hc.client5.http.utils.Base64;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.http.utils.Base64;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.ProtocolException;
 
+/**
+ * @deprecated Use {@link BasicAuthenticationHandler}.
+ */
+@Deprecated
 public class BasicAuthTokenExtractor {
 
     public String extract(final String challengeResponse) throws HttpException {
diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java
similarity index 55%
copy from httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
copy to httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java
index b07dc9ccf..c77e83349 100644
--- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java
@@ -29,32 +29,21 @@ package org.apache.hc.client5.testing.auth;
 
 import java.nio.charset.StandardCharsets;
 
-import org.apache.hc.client5.http.utils.Base64;
 import org.apache.hc.client5.http.auth.StandardAuthScheme;
-import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.client5.http.utils.Base64;
 
-public class BasicAuthTokenExtractor {
+public class BasicAuthenticationHandler extends AbstractAuthenticationHandler {
+
+    @Override
+    String getSchemeName() {
+        return StandardAuthScheme.BASIC;
+    }
 
-    public String extract(final String challengeResponse) throws HttpException {
-        if (challengeResponse != null) {
-            final int i = challengeResponse.indexOf(' ');
-            if (i == -1) {
-                throw new ProtocolException("Invalid challenge response: " + challengeResponse);
-            }
-            final String schemeName = challengeResponse.substring(0, i);
-            if (schemeName.equalsIgnoreCase(StandardAuthScheme.BASIC)) {
-                final String s = challengeResponse.substring(i + 1).trim();
-                try {
-                    final byte[] credsRaw = s.getBytes(StandardCharsets.US_ASCII);
-                    final Base64 codec = new Base64();
-                    return new String(codec.decode(credsRaw), StandardCharsets.US_ASCII);
-                } catch (final IllegalArgumentException ex) {
-                    throw new ProtocolException("Malformed Basic credentials");
-                }
-            }
-        }
-        return null;
+    @Override
+    String decodeChallenge(final String challenge) throws IllegalArgumentException {
+        final byte[] bytes = challenge.getBytes(StandardCharsets.US_ASCII);
+        final Base64 codec = new Base64();
+        return new String(codec.decode(bytes), StandardCharsets.US_ASCII);
     }
 
 }
diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java
index ba5a8b945..e0598043e 100644
--- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java
@@ -28,10 +28,11 @@
 package org.apache.hc.client5.testing.classic;
 
 import java.io.IOException;
+import java.util.Collections;
 
-import org.apache.hc.client5.http.auth.StandardAuthScheme;
+import org.apache.hc.client5.testing.auth.AuthenticationHandler;
 import org.apache.hc.client5.testing.auth.Authenticator;
-import org.apache.hc.client5.testing.auth.BasicAuthTokenExtractor;
+import org.apache.hc.client5.testing.auth.BasicAuthenticationHandler;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
 import org.apache.hc.core5.http.Header;
@@ -42,6 +43,7 @@ import org.apache.hc.core5.http.io.HttpServerRequestHandler;
 import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.io.entity.StringEntity;
 import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
 import org.apache.hc.core5.http.protocol.HttpContext;
 import org.apache.hc.core5.net.URIAuthority;
 import org.apache.hc.core5.util.Args;
@@ -49,13 +51,23 @@ import org.apache.hc.core5.util.Args;
 public class AuthenticatingDecorator implements HttpServerRequestHandler {
 
     private final HttpServerRequestHandler requestHandler;
+    private final AuthenticationHandler<String> authenticationHandler;
     private final Authenticator authenticator;
-    private final BasicAuthTokenExtractor authTokenExtractor;
 
-    public AuthenticatingDecorator(final HttpServerRequestHandler requestHandler, final Authenticator authenticator) {
+    /**
+     * @since 5.3
+     */
+    public AuthenticatingDecorator(final HttpServerRequestHandler requestHandler,
+                                   final AuthenticationHandler<String> authenticationHandler,
+                                   final Authenticator authenticator) {
         this.requestHandler = Args.notNull(requestHandler, "Request handler");
+        this.authenticationHandler = Args.notNull(authenticationHandler, "Authentication handler");
         this.authenticator = Args.notNull(authenticator, "Authenticator");
-        this.authTokenExtractor = new BasicAuthTokenExtractor();
+    }
+
+    public AuthenticatingDecorator(final HttpServerRequestHandler requestHandler,
+                                   final Authenticator authenticator) {
+        this(requestHandler, new BasicAuthenticationHandler(), authenticator);
     }
 
     protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) {
@@ -67,7 +79,7 @@ public class AuthenticatingDecorator implements HttpServerRequestHandler {
             final ResponseTrigger responseTrigger,
             final HttpContext context) throws HttpException, IOException {
         final Header h = request.getFirstHeader(HttpHeaders.AUTHORIZATION);
-        final String challengeResponse = h != null ? authTokenExtractor.extract(h.getValue()) : null;
+        final String challengeResponse = h != null ? authenticationHandler.extractAuthToken(h.getValue()) : null;
 
         final URIAuthority authority = request.getAuthority();
         final String requestUri = request.getRequestUri();
@@ -84,7 +96,9 @@ public class AuthenticatingDecorator implements HttpServerRequestHandler {
         } else {
             final ClassicHttpResponse unauthorized = new BasicClassicHttpResponse(HttpStatus.SC_UNAUTHORIZED);
             final String realm = authenticator.getRealm(authority, requestUri);
-            unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"" + realm + "\"");
+            final String challenge = authenticationHandler.challenge(
+                    realm != null ? Collections.singletonList(new BasicNameValuePair("realm", realm)) : null);
+            unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, challenge);
             customizeUnauthorizedResponse(unauthorized);
             if (unauthorized.getEntity() == null) {
                 unauthorized.setEntity(new StringEntity("Unauthorized"));


[httpcomponents-client] 02/02: 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 a26766d2f57b3760e5d70bd33a9deb0708e4f1a6
Author: Oleg Kalnichevski <ol...@apache.org>
AuthorDate: Sun Nov 27 15:54:04 2022 +0100

    BEARER auth scheme support (RFC 6750)
---
 .../auth/TokenBearerAuthenticationHandler.java     |  44 ++++++
 .../AbstractHttpAsyncClientAuthenticationTest.java |  66 +++++++++
 .../testing/sync/TestClientAuthentication.java     |  72 ++++++++++
 .../hc/client5/http/impl/win/WinHttpClients.java   |   2 +
 .../apache/hc/client5/http/auth/BearerToken.java   | 120 ++++++++++++++++
 .../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    | 154 +++++++++++++++++++++
 .../auth/BearerSchemeFactory.java}                 |  56 +++-----
 .../http/impl/classic/HttpClientBuilder.java       |   2 +
 .../hc/client5/http/auth/TestCredentials.java      |  32 +++++
 .../client5/http/impl/auth/TestBearerScheme.java   | 106 ++++++++++++++
 14 files changed, 629 insertions(+), 37 deletions(-)

diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/TokenBearerAuthenticationHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/TokenBearerAuthenticationHandler.java
new file mode 100644
index 000000000..303062afe
--- /dev/null
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/TokenBearerAuthenticationHandler.java
@@ -0,0 +1,44 @@
+/*
+ * ====================================================================
+ * 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.testing.auth;
+
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+
+public class TokenBearerAuthenticationHandler extends AbstractAuthenticationHandler {
+
+    @Override
+    String getSchemeName() {
+        return StandardAuthScheme.BEARER;
+    }
+
+    @Override
+    String decodeChallenge(final String challenge) throws IllegalArgumentException {
+        return challenge;
+    }
+
+}
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..21ba31496 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.TokenBearerAuthenticationHandler;
 import org.apache.hc.core5.function.Decorator;
 import org.apache.hc.core5.http.ContentType;
 import org.apache.hc.core5.http.HttpHeaders;
@@ -475,4 +478,67 @@ 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 TokenBearerAuthenticationHandler(),
+                        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("some user", 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("some user", 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..6954d8fbb 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.TokenBearerAuthenticationHandler;
 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;
@@ -762,4 +765,73 @@ 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 TokenBearerAuthenticationHandler(),
+                        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("some user", 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("some user", 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..aa6bdf903
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/auth/BearerToken.java
@@ -0,0 +1,120 @@
+/*
+ * ====================================================================
+ * 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 Principal principal;
+    private final String token;
+
+    /**
+     * The constructor with the user principal and token arguments.
+     *
+     * @param userPrincipal the user principal
+     * @param token the token
+     *
+     * @see BasicUserPrincipal
+     * @see NTUserPrincipal
+     */
+    public BearerToken(final Principal userPrincipal, final String token) {
+        super();
+        this.principal = Args.notNull(userPrincipal, "User principal");
+        this.token = Args.notBlank(token, "Token");
+    }
+
+    /**
+     * The constructor with the username and token arguments.
+     *
+     * @param userName the username
+     * @param token the token
+     */
+    public BearerToken(final String userName, final String token) {
+        this(new BasicUserPrincipal(userName), token);
+    }
+
+    @Override
+    public Principal getUserPrincipal() {
+        return this.principal;
+    }
+
+    public String getUserName() {
+        return this.principal.getName();
+    }
+
+    /**
+     * @deprecated Do not use.
+     */
+    @Deprecated
+    @Override
+    public char[] getPassword() {
+        return null;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    @Override
+    public int hashCode() {
+        return this.principal.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.principal, that.principal);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return this.token;
+    }
+
+}
+
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..e5e97b20c
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerScheme.java
@@ -0,0 +1,154 @@
+/*
+ * ====================================================================
+ * 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());
+            }
+        }
+        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;
+    }
+
+    @Override
+    public String toString() {
+        return getName() + this.paramMap;
+    }
+
+}
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/impl/auth/BearerSchemeFactory.java
similarity index 50%
copy from httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
copy to httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
index 51371cc7b..f05f1be14 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/auth/StandardAuthScheme.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
@@ -25,48 +25,32 @@
  *
  */
 
-package org.apache.hc.client5.http.auth;
+package org.apache.hc.client5.http.impl.auth;
+
+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;
 
 /**
- * Authentication schemes by their names supported by the HttpClient.
+ * {@link AuthSchemeFactory} implementation that creates and initializes
+ * {@link BearerScheme} instances.
  *
- * @since 4.3
+ * @since 5.3
  */
-public final class StandardAuthScheme {
-
-    private StandardAuthScheme() {
-        // no instances
-    }
-
-    /**
-     * Basic authentication scheme (considered inherently insecure without transport encryption,
-     * but most widely supported).
-     */
-    public static final String BASIC = "Basic";
+@Contract(threading = ThreadingBehavior.STATELESS)
+public class BearerSchemeFactory implements AuthSchemeFactory {
 
-    /**
-     * Digest authentication scheme.
-     */
-    public static final String DIGEST = "Digest";
+    public static final BearerSchemeFactory INSTANCE = new BearerSchemeFactory();
 
-    /**
-     * The NTLM authentication scheme is a proprietary Microsoft Windows
-     * authentication protocol as defined in [MS-NLMP].
-     */
-    public static final String NTLM = "NTLM";
-
-    /**
-     * SPNEGO authentication scheme as defined in RFC 4559 and RFC 4178
-     * (considered to be the most secure among currently supported
-     * authentication schemes if Kerberos is selected).
-     */
-    public static final String SPNEGO = "Negotiate";
+    public BearerSchemeFactory() {
+        super();
+    }
 
-    /**
-     * Kerberos authentication scheme as defined in RFC 4120
-     * (considered to be the most secure among currently supported
-     * authentication schemes).
-     */
-    public static final String KERBEROS = "Kerberos";
+    @Override
+    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..b82ffc2d3 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,38 @@ public class TestCredentials {
         Assertions.assertEquals(creds1, creds3);
     }
 
+    @Test
+    public void tesBearerTokenBasics() {
+        final BearerToken creds1 = new BearerToken("name", "token of some sort");
+        Assertions.assertEquals("name", creds1.getUserName());
+        Assertions.assertEquals(new BasicUserPrincipal("name"),
+                creds1.getUserPrincipal());
+        Assertions.assertEquals("token of some sort", creds1.getToken());
+        Assertions.assertEquals("token of some sort", creds1.toString());
+    }
+
+    @Test
+    public void testBearerTokenHashCode() {
+        final BearerToken creds1 = new BearerToken("name", "token of some sort");
+        final BearerToken creds2 = new BearerToken("other name", "token of some sort");
+        final BearerToken creds3 = new BearerToken("name", "token of different 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("name", "token of some sort");
+        final BearerToken creds2 = new BearerToken("other name", "token of some sort");
+        final BearerToken creds3 = new BearerToken("name", "token of different 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..035c1d78c
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestBearerScheme.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * 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("somne user", "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("Bearer some token", authResponse);
+        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());
+    }
+
+}