From 665e1fea5ff0a7531b98c3884d250393ec3633d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Czig=C3=A1ny?= Date: Sun, 12 Oct 2025 17:48:58 +0200 Subject: [PATCH] #251: bearer token wire --- .../com/jcabi/http/wire/BearerAuthWire.java | 102 ++++++++++++++++++ .../jcabi/http/wire/BearerAuthWireITCase.java | 73 +++++++++++++ .../jcabi/http/wire/BearerAuthWireTest.java | 86 +++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/main/java/com/jcabi/http/wire/BearerAuthWire.java create mode 100644 src/test/java/com/jcabi/http/wire/BearerAuthWireITCase.java create mode 100644 src/test/java/com/jcabi/http/wire/BearerAuthWireTest.java diff --git a/src/main/java/com/jcabi/http/wire/BearerAuthWire.java b/src/main/java/com/jcabi/http/wire/BearerAuthWire.java new file mode 100644 index 00000000..f4f2864f --- /dev/null +++ b/src/main/java/com/jcabi/http/wire/BearerAuthWire.java @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + */ +package com.jcabi.http.wire; + +import com.jcabi.aspects.Immutable; +import com.jcabi.http.ImmutableHeader; +import com.jcabi.http.Request; +import com.jcabi.http.Response; +import com.jcabi.http.Wire; +import com.jcabi.log.Logger; +import jakarta.ws.rs.core.HttpHeaders; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.LinkedList; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * Wire with HTTP bearer token authentication. + * + *

This wire adds an {@code "Authorization: Bearer ..."} HTTP header + * to the request, if it's not yet provided, for example: + * + *

 String html = new JdkRequest("http://my.example.com")
+ *   .through(BearerAuthWire.class, "mF_9.B5f-4.1JqM")
+ *   .fetch()
+ *   .body();
+ * + *

In this example, an additional HTTP header {@code Authorization} + * will be added with a value {@code Bearer mF_9.B5f-4.1JqM}. + * + *

The class is immutable and thread-safe. + * + * @see RFC 6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage" + * @since 2.0 + */ +@Immutable +@ToString(of = "origin") +@EqualsAndHashCode(of = "origin") +public final class BearerAuthWire implements Wire { + /** + * Authorization header format. + */ + private static final String AUTH_FORMAT = "Bearer %s"; + + /** + * Original wire. + */ + private final transient Wire origin; + + /** + * The bearer token to use. + */ + private final transient String token; + + /** + * Public ctor. + * + * @param origin Orignal wire + * @param token Bearer token + */ + public BearerAuthWire(final Wire origin, final String token) { + this.origin = origin; + this.token = token; + } + + @Override + public Response send( + final Request req, + final String home, + final String method, + final Collection> headers, + final InputStream content, + final int connect, + final int read + ) throws IOException { + final Collection> hdrs = + new LinkedList<>(headers); + if ( + headers.stream() + .noneMatch(h -> h.getKey().equals(HttpHeaders.AUTHORIZATION)) + ) { + hdrs.add( + new ImmutableHeader( + HttpHeaders.AUTHORIZATION, + String.format(BearerAuthWire.AUTH_FORMAT, this.token) + ) + ); + } else { + Logger.warn( + this, + "Request already contains %s header", + HttpHeaders.AUTHORIZATION + ); + } + return this.origin.send(req, home, method, hdrs, content, connect, read); + } +} diff --git a/src/test/java/com/jcabi/http/wire/BearerAuthWireITCase.java b/src/test/java/com/jcabi/http/wire/BearerAuthWireITCase.java new file mode 100644 index 00000000..6c9fc149 --- /dev/null +++ b/src/test/java/com/jcabi/http/wire/BearerAuthWireITCase.java @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + */ +package com.jcabi.http.wire; + +import com.jcabi.http.request.JdkRequest; +import com.jcabi.http.response.XmlResponse; +import java.io.IOException; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Integration case for {@link BearerAuthWire}. + * + * @since 2.0 + */ +final class BearerAuthWireITCase { + @Test + void bearerTokenAuthWorks() throws IOException { + final String token = "t0k3nId"; + final XmlResponse res = new JdkRequest( + "https://authenticationtest.com" + ) + .through(BearerAuthWire.class, token) + .through(AutoRedirectingWire.class) + .fetch() + .as(XmlResponse.class); + MatcherAssert.assertThat( + "token should be set", + res.body(), + Matchers.containsString("Token Set") + ); + } + + @Disabled + @Test + void bearerTokenIsNotSet() throws IOException { + final XmlResponse res = new JdkRequest( + "https://User:Pass@authenticationtest.com" + ) + .through(BasicAuthWire.class) + .through(AutoRedirectingWire.class) + .fetch() + .as(XmlResponse.class); + MatcherAssert.assertThat( + "token should not be set", + res.body(), + Matchers.containsString("Token Not Set") + ); + } + + @Disabled + @Test + void bearerTokenIsNotSetIfOtherAuthHeaderIsSetFirst() throws IOException { + final String token = "t0k3nId"; + final XmlResponse res = new JdkRequest( + "https://User:Pass@authenticationtest.com" + ) + .through(BearerAuthWire.class, token) + .through(BasicAuthWire.class) + .through(AutoRedirectingWire.class) + .fetch() + .as(XmlResponse.class); + MatcherAssert.assertThat( + "token should not be set", + res.body(), + Matchers.containsString("Token Not Set") + ); + } +} diff --git a/src/test/java/com/jcabi/http/wire/BearerAuthWireTest.java b/src/test/java/com/jcabi/http/wire/BearerAuthWireTest.java new file mode 100644 index 00000000..4f69d717 --- /dev/null +++ b/src/test/java/com/jcabi/http/wire/BearerAuthWireTest.java @@ -0,0 +1,86 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2011-2025 Yegor Bugayenko + * SPDX-License-Identifier: MIT + */ +package com.jcabi.http.wire; + +import com.jcabi.http.mock.MkAnswer; +import com.jcabi.http.mock.MkContainer; +import com.jcabi.http.mock.MkGrizzlyContainer; +import com.jcabi.http.request.JdkRequest; +import com.jcabi.http.response.RestResponse; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.net.HttpURLConnection; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; + +/** + * Test case for {@link BearerAuthWire}. + * + * @since 2.0 + */ +final class BearerAuthWireTest { + @Test + void bearerAuthWireWorks() throws IOException { + final String token = "my-bearer-token"; + final String expected = "Bearer my-bearer-token"; + final MkContainer container = new MkGrizzlyContainer().next( + new MkAnswer.Simple("") + ).start(); + new JdkRequest(UriBuilder.fromUri(container.home()).build()) + .through(BearerAuthWire.class, token) + .fetch() + .as(RestResponse.class) + .assertStatus(HttpURLConnection.HTTP_OK); + container.stop(); + MatcherAssert.assertThat( + "should be correct header", + container.take().headers().get(HttpHeaders.AUTHORIZATION).get(0), + Matchers.equalTo(expected) + ); + } + + @Test + void onlyOneBearerAuthWireWorks() throws IOException { + final MkContainer container = new MkGrizzlyContainer().next( + new MkAnswer.Simple("") + ).start(); + new JdkRequest(UriBuilder.fromUri(container.home()).build()) + .through(BearerAuthWire.class, "my-third-bearer-token") + .through(BearerAuthWire.class, "my-second-bearer-token") + .through(BearerAuthWire.class, "my-first-bearer-token") + .fetch() + .as(RestResponse.class) + .assertStatus(HttpURLConnection.HTTP_OK); + container.stop(); + MatcherAssert.assertThat( + "there should be no more than one 'Authorization' header", + container.take().headers().get(HttpHeaders.AUTHORIZATION).size(), + Matchers.equalTo(1) + ); + } + + @Test + void onlyTheFirstBearerAuthWireWorks() throws IOException { + final String expected = "Bearer my-first-bearer-token"; + final MkContainer container = new MkGrizzlyContainer().next( + new MkAnswer.Simple("") + ).start(); + new JdkRequest(UriBuilder.fromUri(container.home()).build()) + .through(BearerAuthWire.class, "my-third-bearer-token") + .through(BearerAuthWire.class, "my-second-bearer-token") + .through(BearerAuthWire.class, "my-first-bearer-token") + .fetch() + .as(RestResponse.class) + .assertStatus(HttpURLConnection.HTTP_OK); + container.stop(); + MatcherAssert.assertThat( + "should be correct header", + container.take().headers().get(HttpHeaders.AUTHORIZATION).get(0), + Matchers.equalTo(expected) + ); + } +}