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()); + } + +}