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