From a4784916cc1a0d67dc0d4e945102145e3c9a4f68 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Sun, 27 Nov 2022 15:04:24 +0100 Subject: [PATCH] Made authenticating decorators capable of supporting different authentication schemes --- .../async/AuthenticatingAsyncDecorator.java | 28 +++++-- .../auth/AbstractAuthenticationHandler.java | 77 +++++++++++++++++++ .../testing/auth/AuthenticationHandler.java | 44 +++++++++++ .../testing/auth/BasicAuthTokenExtractor.java | 6 +- .../auth/BasicAuthenticationHandler.java | 49 ++++++++++++ .../classic/AuthenticatingDecorator.java | 28 +++++-- 6 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java create mode 100644 httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java create mode 100644 httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java index cad07b812..f0bff871d 100644 --- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/async/AuthenticatingAsyncDecorator.java @@ -28,12 +28,13 @@ import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.testing.auth.AuthenticationHandler; import org.apache.hc.client5.testing.auth.Authenticator; -import org.apache.hc.client5.testing.auth.BasicAuthTokenExtractor; +import org.apache.hc.client5.testing.auth.BasicAuthenticationHandler; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; @@ -44,6 +45,7 @@ import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.apache.hc.core5.http.message.BasicHttpResponse; +import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.http.nio.AsyncResponseProducer; import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler; import org.apache.hc.core5.http.nio.CapacityChannel; @@ -58,15 +60,24 @@ public class AuthenticatingAsyncDecorator implements AsyncServerExchangeHandler { private final AsyncServerExchangeHandler exchangeHandler; + private final AuthenticationHandler authenticationHandler; private final Authenticator authenticator; private final AtomicReference responseProducerRef; - private final BasicAuthTokenExtractor authTokenExtractor; - public AuthenticatingAsyncDecorator(final AsyncServerExchangeHandler exchangeHandler, final Authenticator authenticator) { + /** + * @since 5.3 + */ + public AuthenticatingAsyncDecorator(final AsyncServerExchangeHandler exchangeHandler, + final AuthenticationHandler authenticationHandler, + final Authenticator authenticator) { this.exchangeHandler = Args.notNull(exchangeHandler, "Request handler"); + this.authenticationHandler = Args.notNull(authenticationHandler, "Authentication handler"); this.authenticator = Args.notNull(authenticator, "Authenticator"); this.responseProducerRef = new AtomicReference<>(); - this.authTokenExtractor = new BasicAuthTokenExtractor(); + } + + public AuthenticatingAsyncDecorator(final AsyncServerExchangeHandler exchangeHandler, final Authenticator authenticator) { + this(exchangeHandler, new BasicAuthenticationHandler(), authenticator); } protected void customizeUnauthorizedResponse(final HttpResponse unauthorized) { @@ -79,7 +90,7 @@ public void handleRequest( final ResponseChannel responseChannel, final HttpContext context) throws HttpException, IOException { final Header h = request.getFirstHeader(HttpHeaders.AUTHORIZATION); - final String challengeResponse = h != null ? authTokenExtractor.extract(h.getValue()) : null; + final String challengeResponse = h != null ? authenticationHandler.extractAuthToken(h.getValue()) : null; final URIAuthority authority = request.getAuthority(); final String requestUri = request.getRequestUri(); @@ -96,8 +107,9 @@ public void handleRequest( } else { final HttpResponse unauthorized = new BasicHttpResponse(HttpStatus.SC_UNAUTHORIZED); final String realm = authenticator.getRealm(authority, requestUri); - unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"" + realm + "\""); - + final String challenge = authenticationHandler.challenge( + realm != null ? Collections.singletonList(new BasicNameValuePair("realm", realm)) : null); + unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, challenge); customizeUnauthorizedResponse(unauthorized); final AsyncResponseProducer responseProducer = new BasicResponseProducer( diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java new file mode 100644 index 000000000..7ba77f3b4 --- /dev/null +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AbstractAuthenticationHandler.java @@ -0,0 +1,77 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.testing.auth; + +import java.util.List; + +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.ProtocolException; + +abstract class AbstractAuthenticationHandler implements AuthenticationHandler { + + abstract String getSchemeName(); + + @Override + public final String challenge(final List params) { + final StringBuilder buf = new StringBuilder(); + buf.append(getSchemeName()); + if (params != null && params.size() > 0) { + buf.append(" "); + for (int i = 0; i < params.size(); i++) { + if (i > 0) { + buf.append(", "); + } + final NameValuePair param = params.get(i); + buf.append(param.getName()).append("=\"").append(param.getValue()).append("\""); + } + } + return buf.toString(); + } + + abstract String decodeChallenge(String challenge) throws IllegalArgumentException; + + public final String extractAuthToken(final String challengeResponse) throws HttpException { + final int i = challengeResponse.indexOf(' '); + if (i == -1) { + throw new ProtocolException("Invalid " + getSchemeName() + " challenge response"); + } + final String schemeName = challengeResponse.substring(0, i); + if (schemeName.equalsIgnoreCase(getSchemeName())) { + final String s = challengeResponse.substring(i + 1).trim(); + try { + return decodeChallenge(s); + } catch (final IllegalArgumentException ex) { + throw new ProtocolException("Malformed " + getSchemeName() + " credentials"); + } + } else { + throw new ProtocolException("Unexpected challenge type"); + } + } + +} diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java new file mode 100644 index 000000000..644c29729 --- /dev/null +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/AuthenticationHandler.java @@ -0,0 +1,44 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.testing.auth; + +import java.util.List; + +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.NameValuePair; + +/** + * @since 5.3 + */ +public interface AuthenticationHandler { + + String challenge(List params); + + T extractAuthToken(String challengeResponse) throws HttpException; + +} diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java index b07dc9ccf..1ea4ab3c5 100644 --- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthTokenExtractor.java @@ -29,11 +29,15 @@ import java.nio.charset.StandardCharsets; -import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.utils.Base64; import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.ProtocolException; +/** + * @deprecated Use {@link BasicAuthenticationHandler}. + */ +@Deprecated public class BasicAuthTokenExtractor { public String extract(final String challengeResponse) throws HttpException { diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java new file mode 100644 index 000000000..c77e83349 --- /dev/null +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/auth/BasicAuthenticationHandler.java @@ -0,0 +1,49 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.testing.auth; + +import java.nio.charset.StandardCharsets; + +import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.utils.Base64; + +public class BasicAuthenticationHandler extends AbstractAuthenticationHandler { + + @Override + String getSchemeName() { + return StandardAuthScheme.BASIC; + } + + @Override + String decodeChallenge(final String challenge) throws IllegalArgumentException { + final byte[] bytes = challenge.getBytes(StandardCharsets.US_ASCII); + final Base64 codec = new Base64(); + return new String(codec.decode(bytes), StandardCharsets.US_ASCII); + } + +} diff --git a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java index ba5a8b945..e0598043e 100644 --- a/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java +++ b/httpclient5-testing/src/main/java/org/apache/hc/client5/testing/classic/AuthenticatingDecorator.java @@ -28,10 +28,11 @@ package org.apache.hc.client5.testing.classic; import java.io.IOException; +import java.util.Collections; -import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.testing.auth.AuthenticationHandler; import org.apache.hc.client5.testing.auth.Authenticator; -import org.apache.hc.client5.testing.auth.BasicAuthTokenExtractor; +import org.apache.hc.client5.testing.auth.BasicAuthenticationHandler; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; @@ -42,6 +43,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; +import org.apache.hc.core5.http.message.BasicNameValuePair; import org.apache.hc.core5.http.protocol.HttpContext; import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.util.Args; @@ -49,13 +51,23 @@ public class AuthenticatingDecorator implements HttpServerRequestHandler { private final HttpServerRequestHandler requestHandler; + private final AuthenticationHandler authenticationHandler; private final Authenticator authenticator; - private final BasicAuthTokenExtractor authTokenExtractor; - public AuthenticatingDecorator(final HttpServerRequestHandler requestHandler, final Authenticator authenticator) { + /** + * @since 5.3 + */ + public AuthenticatingDecorator(final HttpServerRequestHandler requestHandler, + final AuthenticationHandler authenticationHandler, + final Authenticator authenticator) { this.requestHandler = Args.notNull(requestHandler, "Request handler"); + this.authenticationHandler = Args.notNull(authenticationHandler, "Authentication handler"); this.authenticator = Args.notNull(authenticator, "Authenticator"); - this.authTokenExtractor = new BasicAuthTokenExtractor(); + } + + public AuthenticatingDecorator(final HttpServerRequestHandler requestHandler, + final Authenticator authenticator) { + this(requestHandler, new BasicAuthenticationHandler(), authenticator); } protected void customizeUnauthorizedResponse(final ClassicHttpResponse unauthorized) { @@ -67,7 +79,7 @@ public void handle( final ResponseTrigger responseTrigger, final HttpContext context) throws HttpException, IOException { final Header h = request.getFirstHeader(HttpHeaders.AUTHORIZATION); - final String challengeResponse = h != null ? authTokenExtractor.extract(h.getValue()) : null; + final String challengeResponse = h != null ? authenticationHandler.extractAuthToken(h.getValue()) : null; final URIAuthority authority = request.getAuthority(); final String requestUri = request.getRequestUri(); @@ -84,7 +96,9 @@ public void handle( } else { final ClassicHttpResponse unauthorized = new BasicClassicHttpResponse(HttpStatus.SC_UNAUTHORIZED); final String realm = authenticator.getRealm(authority, requestUri); - unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=\"" + realm + "\""); + final String challenge = authenticationHandler.challenge( + realm != null ? Collections.singletonList(new BasicNameValuePair("realm", realm)) : null); + unauthorized.addHeader(HttpHeaders.WWW_AUTHENTICATE, challenge); customizeUnauthorizedResponse(unauthorized); if (unauthorized.getEntity() == null) { unauthorized.setEntity(new StringEntity("Unauthorized"));