diff --git a/CHANGES.md b/CHANGES.md index e3e5806f..833a4552 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,6 @@ +### Version 5.4.0 +* Add `BasicAuthRequestInterceptor` + ### Version 5.3.0 * Split `GsonCodec` into `GsonEncoder` and `GsonDecoder`, which are easy to use with `Feign.Builder` * Deprecate `GsonCodec` diff --git a/README.md b/README.md index 622d4581..1d43eacd 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ For further flexibility, you can use Dagger modules directly. See the `Dagger` When you need to change all requests, regardless of their target, you'll want to configure a `RequestInterceptor`. For example, if you are acting as an intermediary, you might want to propagate the `X-Forwarded-For` header. -``` +```java static class ForwardedForInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { template.header("X-Forwarded-For", "origin.host.com"); @@ -66,6 +66,12 @@ static class ForwardedForInterceptor implements RequestInterceptor { Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new ForwardedForInterceptor()).target(Bank.class, "https://api.examplebank.com"); ``` +Another common example of an interceptor would be authentication, such as using the built-in `BasicAuthRequestInterceptor`. + +```java +Bank bank = Feign.builder().decoder(accountDecoder).requestInterceptor(new BasicAuthRequestInterceptor(username, password)).target(Bank.class, "https://api.examplebank.com"); +``` + ### Multiple Interfaces Feign can produce multiple api interfaces. These are defined as `Target` (default `HardCodedTarget`), which allow for dynamic discovery and decoration of requests prior to execution. diff --git a/core/src/main/java/feign/Util.java b/core/src/main/java/feign/Util.java index f3b7b0ac..0036be7b 100644 --- a/core/src/main/java/feign/Util.java +++ b/core/src/main/java/feign/Util.java @@ -60,6 +60,10 @@ public class Util { * UTF-8: eight-bit UCS Transformation Format. */ public static final Charset UTF_8 = Charset.forName("UTF-8"); + /** + * ISO-8859-1: ISO Latin Alphabet Number 1 (ISO-LATIN-1). + */ + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); /** * Copy of {@code com.google.common.base.Preconditions#checkArgument}. diff --git a/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java new file mode 100644 index 00000000..b0a2ee9e --- /dev/null +++ b/core/src/main/java/feign/auth/BasicAuthRequestInterceptor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed 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. + */ +package feign.auth; + +import feign.RequestInterceptor; +import feign.RequestTemplate; +import sun.misc.BASE64Encoder; + +import java.nio.charset.Charset; + +import static feign.Util.checkNotNull; +import static feign.Util.ISO_8859_1; + +/** + * An interceptor that adds the request header needed to use HTTP basic authentication. + */ +public class BasicAuthRequestInterceptor implements RequestInterceptor { + private final String headerValue; + + /** + * Creates an interceptor that authenticates all requests with the specified username and password encoded using + * ISO-8859-1. + * + * @param username the username to use for authentication + * @param password the password to use for authentication + */ + public BasicAuthRequestInterceptor(String username, String password) { + this(username, password, ISO_8859_1); + } + + /** + * Creates an interceptor that authenticates all requests with the specified username and password encoded using + * the specified charset. + * + * @param username the username to use for authentication + * @param password the password to use for authentication + * @param charset the charset to use when encoding the credentials + */ + public BasicAuthRequestInterceptor(String username, String password, Charset charset) { + checkNotNull(username, "username"); + checkNotNull(password, "password"); + this.headerValue = "Basic " + base64Encode((username + ":" + password).getBytes(charset)); + } + + @Override public void apply(RequestTemplate template) { + template.header("Authorization", headerValue); + } + + /* + * This uses a Sun internal method; if we ever encounter a case where this method is not available, the appropriate + * response would be to pull the necessary portions of Guava's BaseEncoding class into Util. + */ + private static String base64Encode(byte[] bytes) { + return new BASE64Encoder().encode(bytes); + } +} diff --git a/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java new file mode 100644 index 00000000..3a8c6bf5 --- /dev/null +++ b/core/src/test/java/feign/auth/BasicAuthRequestInterceptorTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Netflix, Inc. + * + * Licensed 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. + */ +package feign.auth; + +import feign.RequestTemplate; +import org.testng.annotations.Test; + +import java.util.Collection; +import java.util.Collections; + +import static org.testng.Assert.assertEquals; + +/** + * Tests for {@link BasicAuthRequestInterceptor}. + */ +public class BasicAuthRequestInterceptorTest { + /** + * Tests that request headers are added as expected. + */ + @Test public void testAuthentication() { + RequestTemplate template = new RequestTemplate(); + BasicAuthRequestInterceptor interceptor = new BasicAuthRequestInterceptor("Aladdin", "open sesame"); + interceptor.apply(template); + Collection actualValue = template.headers().get("Authorization"); + Collection expectedValue = Collections.singletonList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); + assertEquals(actualValue, expectedValue); + } +}