/*
* Copyright 2016 LINE Corporation
*
* LINE Corporation 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.
*/
package com.linecorp.armeria.server.http.auth;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.assertj.core.util.Strings;
import org.junit.ClassRule;
import org.junit.Test;
import com.google.common.collect.ImmutableMap;
import com.linecorp.armeria.common.http.HttpHeaderNames;
import com.linecorp.armeria.common.http.HttpRequest;
import com.linecorp.armeria.common.http.HttpResponse;
import com.linecorp.armeria.common.http.HttpResponseWriter;
import com.linecorp.armeria.common.http.HttpStatus;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.http.AbstractHttpService;
import com.linecorp.armeria.server.http.HttpService;
import com.linecorp.armeria.server.logging.LoggingService;
import com.linecorp.armeria.testing.server.ServerRule;
public class HttpAuthServiceTest {
private static final Encoder BASE64_ENCODER = Base64.getEncoder();
@ClassRule
public static final ServerRule server = new ServerRule() {
@Override
protected void configure(ServerBuilder sb) throws Exception {
final HttpService ok = new AbstractHttpService() {
@Override
protected void doGet(
ServiceRequestContext ctx, HttpRequest req, HttpResponseWriter res) {
res.respond(HttpStatus.OK);
}
};
// Auth with arbitrary authorizer
Authorizer<HttpRequest> authorizer = (ctx, req) ->
CompletableFuture.supplyAsync(
() -> "unit test".equals(req.headers().get(HttpHeaderNames.AUTHORIZATION)));
sb.serviceAt(
"/",
ok.decorate(HttpAuthService.newDecorator(authorizer))
.decorate(LoggingService::new));
// Auth with HTTP basic
final Map<String, String> usernameToPassword = ImmutableMap.of("brown", "cony", "pangyo", "choco");
Authorizer<BasicToken> httpBasicAuthorizer = (ctx, token) -> {
String username = token.username();
String password = token.password();
return completedFuture(password.equals(usernameToPassword.get(username)));
};
sb.serviceAt(
"/basic",
ok.decorate(new HttpAuthServiceBuilder().addBasicAuth(httpBasicAuthorizer).newDecorator())
.decorate(LoggingService::new));
// Auth with OAuth1a
Authorizer<OAuth1aToken> oAuth1aAuthorizer = (ctx, token) ->
completedFuture("dummy_signature".equals(token.signature()));
sb.serviceAt(
"/oauth1a",
ok.decorate(new HttpAuthServiceBuilder().addOAuth1a(oAuth1aAuthorizer).newDecorator())
.decorate(LoggingService::new));
// Auth with OAuth2
Authorizer<OAuth2Token> oAuth2aAuthorizer = (ctx, token) ->
completedFuture("dummy_oauth2_token".equals(token.accessToken()));
sb.serviceAt(
"/oauth2",
ok.decorate(new HttpAuthServiceBuilder().addOAuth2(oAuth2aAuthorizer).newDecorator())
.decorate(LoggingService::new));
// Auth with all predicates above!
sb.serviceAt(
"/composite",
new HttpAuthServiceBuilder().add(authorizer)
.addBasicAuth(httpBasicAuthorizer)
.addOAuth1a(oAuth1aAuthorizer)
.addOAuth2(oAuth2aAuthorizer)
.build(ok)
.decorate(LoggingService::new));
// Authorizer fails with an exception.
sb.serviceAt(
"/authorizer_exception",
ok.decorate(new HttpAuthServiceBuilder().add((ctx, data) -> {
throw new RuntimeException("bug!");
}).newDecorator())
.decorate(LoggingService::new));
// AuthService fails when building a success message.
sb.serviceAt(
"/on_success_exception",
ok.decorate(service -> new HttpAuthService(service) {
@Override
protected CompletionStage<Boolean> authorize(HttpRequest request,
ServiceRequestContext ctx) {
return completedFuture(true);
}
@Override
protected HttpResponse onSuccess(ServiceRequestContext ctx, HttpRequest req) {
throw new RuntimeException("bug!");
}
}).decorate(LoggingService::new));
}
};
@Test
public void testAuth() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
try (CloseableHttpResponse res = hc.execute(
getRequest("/", "unit test"))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(
getRequest("/", "UNIT TEST"))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
}
}
@Test
public void testBasicAuth() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
try (CloseableHttpResponse res = hc.execute(
basicGetRequest("/basic", BasicToken.of("brown", "cony")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(
basicGetRequest("/basic", BasicToken.of("pangyo", "choco")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(new HttpGet(server.uri("/basic")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
try (CloseableHttpResponse res = hc.execute(
basicGetRequest("/basic", BasicToken.of("choco", "pangyo")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
}
}
@Test
public void testOAuth1a() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
Map<String, String> passToken = ImmutableMap.<String, String>builder()
.put("realm", "dummy_realm")
.put("oauth_consumer_key", "dummy_consumer_key")
.put("oauth_token", "dummy_oauth1a_token")
.put("oauth_signature_method", "dummy")
.put("oauth_signature", "dummy_signature")
.put("oauth_timestamp", "0")
.put("oauth_nonce", "dummy_nonce")
.put("version", "1.0")
.build();
try (CloseableHttpResponse res = hc.execute(
oauth1aGetRequest("/oauth1a", OAuth1aToken.of(passToken)))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
Map<String, String> failToken = ImmutableMap.<String, String>builder()
.put("realm", "dummy_realm")
.put("oauth_consumer_key", "dummy_consumer_key")
.put("oauth_token", "dummy_oauth1a_token")
.put("oauth_signature_method", "dummy")
.put("oauth_signature", "DUMMY_signature")
.put("oauth_timestamp", "0")
.put("oauth_nonce", "dummy_nonce")
.put("version", "1.0")
.build();
try (CloseableHttpResponse res = hc.execute(
oauth1aGetRequest("/oauth1a", OAuth1aToken.of(failToken)))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
}
}
@Test
public void testOAuth2() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
try (CloseableHttpResponse res = hc.execute(
oauth2GetRequest("/oauth2", OAuth2Token.of("dummy_oauth2_token")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(
oauth2GetRequest("/oauth2", OAuth2Token.of("DUMMY_oauth2_token")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
}
}
@Test
public void testCompositeAuth() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
try (CloseableHttpResponse res = hc.execute(
getRequest("/composite", "unit test"))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(
basicGetRequest("/composite", BasicToken.of("brown", "cony")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
Map<String, String> passToken = ImmutableMap.<String, String>builder()
.put("realm", "dummy_realm")
.put("oauth_consumer_key", "dummy_consumer_key")
.put("oauth_token", "dummy_oauth1a_token")
.put("oauth_signature_method", "dummy")
.put("oauth_signature", "dummy_signature")
.put("oauth_timestamp", "0")
.put("oauth_nonce", "dummy_nonce")
.put("version", "1.0")
.build();
try (CloseableHttpResponse res = hc.execute(
oauth1aGetRequest("/composite", OAuth1aToken.of(passToken)))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(
oauth2GetRequest("/composite", OAuth2Token.of("dummy_oauth2_token")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 200 OK"));
}
try (CloseableHttpResponse res = hc.execute(new HttpGet(server.uri("/composite")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
try (CloseableHttpResponse res = hc.execute(
basicGetRequest("/composite", BasicToken.of("choco", "pangyo")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
}
}
@Test
public void testAuthorizerException() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
try (CloseableHttpResponse res = hc.execute(new HttpGet(server.uri("/authorizer_exception")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 401 Unauthorized"));
}
}
}
@Test
public void testOnSuccessException() throws Exception {
try (CloseableHttpClient hc = HttpClients.createMinimal()) {
try (CloseableHttpResponse res = hc.execute(new HttpGet(server.uri("/on_success_exception")))) {
assertThat(res.getStatusLine().toString(), is("HTTP/1.1 500 Internal Server Error"));
}
}
}
private static HttpRequestBase getRequest(String path, String authorization) {
HttpGet request = new HttpGet(server.uri(path));
request.addHeader("Authorization", authorization);
return request;
}
private static HttpRequestBase basicGetRequest(String path, BasicToken basicToken) {
HttpGet request = new HttpGet(server.uri(path));
request.addHeader("Authorization", "Basic " +
BASE64_ENCODER.encodeToString(
(basicToken.username() + ':' + basicToken.password())
.getBytes(StandardCharsets.US_ASCII)));
return request;
}
private static HttpRequestBase oauth1aGetRequest(String path, OAuth1aToken oAuth1aToken) {
HttpGet request = new HttpGet(server.uri(path));
StringBuilder authorization = new StringBuilder("OAuth ");
String realm = oAuth1aToken.realm();
if (!Strings.isNullOrEmpty(realm)) {
authorization.append("realm=\"");
authorization.append(realm);
authorization.append("\",");
}
authorization.append("oauth_consumer_key=\"");
authorization.append(oAuth1aToken.consumerKey());
authorization.append("\",oauth_token=\"");
authorization.append(oAuth1aToken.token());
authorization.append("\",oauth_signature_method=\"");
authorization.append(oAuth1aToken.signatureMethod());
authorization.append("\",oauth_signature=\"");
authorization.append(oAuth1aToken.signature());
authorization.append("\",oauth_timestamp=\"");
authorization.append(oAuth1aToken.timestamp());
authorization.append("\",oauth_nonce=\"");
authorization.append(oAuth1aToken.nonce());
authorization.append("\",version=\"");
authorization.append(oAuth1aToken.version());
authorization.append('"');
for (Entry<String, String> entry : oAuth1aToken.additionals().entrySet()) {
authorization.append("\",");
authorization.append(entry.getKey());
authorization.append("=\"");
authorization.append(entry.getValue());
authorization.append('"');
}
request.addHeader("Authorization", authorization.toString());
return request;
}
private static HttpRequestBase oauth2GetRequest(String path, OAuth2Token oAuth2Token) {
HttpGet request = new HttpGet(server.uri(path));
request.addHeader("Authorization", "Bearer " + oAuth2Token.accessToken());
return request;
}
}