diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BearerAuthenticationHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BearerAuthenticationHandler.java
new file mode 100644
index 000000000..0f2362fd7
--- /dev/null
+++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BearerAuthenticationHandler.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
+ * .
+ *
+ */
+
+package org.apache.hc.client5.testing.auth;
+
+import org.apache.hc.client5.http.auth.StandardAuthScheme;
+
+public class BearerAuthenticationHandler extends AbstractAuthenticationHandler {
+
+ @Override
+ String getSchemeName() {
+ return StandardAuthScheme.BEARER;
+ }
+
+ @Override
+ 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
+ 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 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 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 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 authSchemeRegistry = RegistryBuilder.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
+ * .
+ *
+ */
+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.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.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
+ * .
+ *
+ */
+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 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 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/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
new file mode 100644
index 000000000..f05f1be14
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/BearerSchemeFactory.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * 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
+ * .
+ *
+ */
+
+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;
+
+/**
+ * {@link AuthSchemeFactory} implementation that creates and initializes
+ * {@link BearerScheme} instances.
+ *
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.STATELESS)
+public class BearerSchemeFactory implements AuthSchemeFactory {
+
+ public static final BearerSchemeFactory INSTANCE = new BearerSchemeFactory();
+
+ public BearerSchemeFactory() {
+ super();
+ }
+
+ @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.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
+ * .
+ *
+ */
+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());
+ }
+
+}