Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/main/java/com/jcabi/http/wire/BearerAuthWire.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>This wire adds an {@code "Authorization: Bearer ..."} HTTP header
* to the request, if it's not yet provided, for example:
*
* <pre> String html = new JdkRequest("http://my.example.com")
* .through(BearerAuthWire.class, "mF_9.B5f-4.1JqM")
* .fetch()
* .body();</pre>
*
* <p>In this example, an additional HTTP header {@code Authorization}
* will be added with a value {@code Bearer mF_9.B5f-4.1JqM}.
*
* <p>The class is immutable and thread-safe.
*
* @see <a href="http://tools.ietf.org/html/rfc6750">RFC 6750 "The OAuth 2.0 Authorization Framework: Bearer Token Usage"</a>
* @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<Map.Entry<String, String>> headers,
final InputStream content,
final int connect,
final int read
) throws IOException {
final Collection<Map.Entry<String, String>> 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);
}
}
73 changes: 73 additions & 0 deletions src/test/java/com/jcabi/http/wire/BearerAuthWireITCase.java
Original file line number Diff line number Diff line change
@@ -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")
);
}
}
86 changes: 86 additions & 0 deletions src/test/java/com/jcabi/http/wire/BearerAuthWireTest.java
Original file line number Diff line number Diff line change
@@ -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)
);
}
}
Loading