Skip to content

Commit 3c38ed5

Browse files
[feat][broker] Update AuthenticationProvider to simplify HTTP Authn (#19197)
PIP: #12105 ### Motivation This is the first of several PRs to implement [PIP 97](#12105). This PR seeks to start to solve the fact that the `AuthenticationState` class currently authenticates `authData` twice instead of just once. This change is important to make before we are able to utilize the async methods introduced in #12104. Historical context: #14044 introduced the `AuthenticationProvider#newHttpAuthState` method. The only use case for this method in the pulsar code base is to let custom providers specify the `AuthenticationDataSource` on http request attributes. The primary problem with that implementation is that the `AuthenticationState` class currently involves authenticating the `authData` passed in to the `newHttpAuthState`. As such, this code is sub-optimal, and creates a confusing flow. I propose we deprecate the `newHttpAuthState` method and instead start using the `authenticateHttpRequestAsync` and `authenticateHttpRequest` methods to allow custom implementations to configure the desired `AuthenticationDataSource` on the request attributes. In order to simplify the diff for reviewers, this PR uses the deprecated `AuthenticationProvider#authenticateHttpRequest` method. I plan to follow up and switch to use the `AuthenticationProvider#authenticateHttpRequestAsync` method. Note that these changes are completely backwards compatible. The only risk is to users that have custom code loaded into the broker that calls the `AuthenticationProvider#authenticateHttpRequest` method. ### Modifications * Deprecate `AuthenticationService#authenticateHttpRequest(HttpServletRequest request, AuthenticationDataSource authData)`. It is no longer used. * Deprecate `AuthenticationProvider#newHttpAuthState(HttpServletRequest request)`. It is no longer used outside of the `AuthenticationProvider` interface itself. * Remove `@InterfaceStability.Unstable` annotation from the `authenticateHttpRequestAsync` method. When I introduced that annotation, I was under the impression that we didn't need it. However, in order to meet the requirements introduced in #14044, we need to let custom `AuthenticationProviders` add their own attributes. * Update the default implementation of `authenticateHttpRequest`. Because the previous implementation was unreachable by all auth providers except for the SASL implementation, this is a backwards compatible change. * Implement changes for the `AuthenticationProviderToken` so that it no longer relies on `newHttpAuthState`. ### Verifying this change I added new tests. ### Does this pull request potentially affect one of the following parts: - [x] The public API This changes the public API within the broker by marking some methods as `@Deprecated`. ### Documentation - [x] `doc-not-needed` We document the `AuthenticationProvider` interface in the code. I added these docs. There is currently no where else to update docs. ### Matching PR in forked repository PR in forked repository: michaeljmarshall#12 ### Additional motivation from PR discussion My primary motivation for this PR is to implement PIP 97. If we want authentication to be asynchronous, we cannot assume that when the `AuthenticationState` object is initialized, the `authenticationDataSource` and the `authRole` are present because the authentication might not yet be completed. My goal with this PR is to break an unnecessary relationship between `AuthenticationState` and http request authentication that was created by #14044.
1 parent ea4f7eb commit 3c38ed5

File tree

6 files changed

+226
-68
lines changed

6 files changed

+226
-68
lines changed

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProvider.java

+27-10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.pulsar.broker.authentication;
2020

21+
import static org.apache.pulsar.broker.web.AuthenticationFilter.AuthenticatedDataAttributeName;
22+
import static org.apache.pulsar.broker.web.AuthenticationFilter.AuthenticatedRoleAttributeName;
2123
import java.io.Closeable;
2224
import java.io.IOException;
2325
import java.net.SocketAddress;
@@ -28,7 +30,6 @@
2830
import javax.servlet.http.HttpServletResponse;
2931
import org.apache.pulsar.broker.ServiceConfiguration;
3032
import org.apache.pulsar.common.api.AuthData;
31-
import org.apache.pulsar.common.classification.InterfaceStability;
3233
import org.apache.pulsar.common.util.FutureUtil;
3334

3435
/**
@@ -103,7 +104,16 @@ default AuthenticationState newAuthState(AuthData authData,
103104

104105
/**
105106
* Create an http authentication data State use passed in AuthenticationDataSource.
107+
* @deprecated implementations that previously relied on this should update their implementation of
108+
* {@link #authenticateHttpRequest(HttpServletRequest, HttpServletResponse)} or of
109+
* {@link #authenticateHttpRequestAsync(HttpServletRequest, HttpServletResponse)} so that the desired attributes
110+
* are added in those methods.
111+
*
112+
* <p>Note: this method was only ever used to generate an {@link AuthenticationState} object in order to generate
113+
* an {@link AuthenticationDataSource} that was added as the {@link AuthenticatedDataAttributeName} attribute to
114+
* the http request. Removing this method removes an unnecessary step in the authentication flow.</p>
106115
*/
116+
@Deprecated(since = "2.12.0")
107117
default AuthenticationState newHttpAuthState(HttpServletRequest request)
108118
throws AuthenticationException {
109119
return new OneStageAuthenticationState(request, this);
@@ -112,20 +122,17 @@ default AuthenticationState newHttpAuthState(HttpServletRequest request)
112122
/**
113123
* Validate the authentication for the given credentials with the specified authentication data.
114124
*
125+
* <p>Implementations of this method MUST modify the request by adding the {@link AuthenticatedRoleAttributeName}
126+
* and the {@link AuthenticatedDataAttributeName} attributes.</p>
127+
*
115128
* <p>Warning: the calling thread is an IO thread. Any implementations that rely on blocking behavior
116129
* must ensure that the execution is completed on using a separate thread pool to ensure IO threads
117130
* are never blocked.</p>
118131
*
119-
* <p>Note: this method is marked as unstable because the Pulsar code base only calls it for the
120-
* Pulsar Broker Auth SASL plugin. All non SASL HTTP requests are authenticated using the
121-
* {@link AuthenticationProvider#authenticateAsync(AuthenticationDataSource)} method. As such,
122-
* this method might be removed in favor of the SASL provider implementing the
123-
* {@link AuthenticationProvider#authenticateAsync(AuthenticationDataSource)} method.</p>
124-
*
125-
* @return Set response, according to passed in request.
132+
* @return Set response, according to passed in request, and return whether we should do following chain.doFilter.
133+
* @throws Exception when authentication failed
126134
* and return whether we should do following chain.doFilter or not.
127135
*/
128-
@InterfaceStability.Unstable
129136
default CompletableFuture<Boolean> authenticateHttpRequestAsync(HttpServletRequest request,
130137
HttpServletResponse response) {
131138
try {
@@ -138,10 +145,20 @@ default CompletableFuture<Boolean> authenticateHttpRequestAsync(HttpServletReque
138145
/**
139146
* Set response, according to passed in request.
140147
* and return whether we should do following chain.doFilter or not.
148+
*
149+
* <p>Implementations of this method MUST modify the request by adding the {@link AuthenticatedRoleAttributeName}
150+
* and the {@link AuthenticatedDataAttributeName} attributes.</p>
151+
*
152+
* @return Set response, according to passed in request, and return whether we should do following chain.doFilter.
153+
* @throws Exception when authentication failed
141154
* @deprecated use and implement {@link AuthenticationProvider#authenticateHttpRequestAsync} instead.
142155
*/
143156
@Deprecated
144157
default boolean authenticateHttpRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
145-
throw new AuthenticationException("Not supported");
158+
AuthenticationState authenticationState = newHttpAuthState(request);
159+
String role = authenticate(authenticationState.getAuthDataSource());
160+
request.setAttribute(AuthenticatedRoleAttributeName, role);
161+
request.setAttribute(AuthenticatedDataAttributeName, authenticationState.getAuthDataSource());
162+
return true;
146163
}
147164
}

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationProviderToken.java

+17
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
package org.apache.pulsar.broker.authentication;
2020

2121
import static java.nio.charset.StandardCharsets.UTF_8;
22+
import static org.apache.pulsar.broker.web.AuthenticationFilter.AuthenticatedDataAttributeName;
23+
import static org.apache.pulsar.broker.web.AuthenticationFilter.AuthenticatedRoleAttributeName;
2224
import com.google.common.annotations.VisibleForTesting;
2325
import io.jsonwebtoken.Claims;
2426
import io.jsonwebtoken.ExpiredJwtException;
@@ -39,6 +41,7 @@
3941
import javax.naming.AuthenticationException;
4042
import javax.net.ssl.SSLSession;
4143
import javax.servlet.http.HttpServletRequest;
44+
import javax.servlet.http.HttpServletResponse;
4245
import org.apache.commons.lang3.StringUtils;
4346
import org.apache.pulsar.broker.ServiceConfiguration;
4447
import org.apache.pulsar.broker.authentication.metrics.AuthenticationMetrics;
@@ -160,6 +163,20 @@ public String authenticate(AuthenticationDataSource authData) throws Authenticat
160163
}
161164
}
162165

166+
@Override
167+
public boolean authenticateHttpRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
168+
HttpServletRequestWrapper wrappedRequest = new HttpServletRequestWrapper(request);
169+
String httpHeaderValue = wrappedRequest.getHeader(HTTP_HEADER_NAME);
170+
if (httpHeaderValue == null || !httpHeaderValue.startsWith(HTTP_HEADER_VALUE_PREFIX)) {
171+
throw new AuthenticationException("Invalid HTTP Authorization header");
172+
}
173+
AuthenticationDataSource authenticationDataSource = new AuthenticationDataHttps(wrappedRequest);
174+
String role = authenticate(authenticationDataSource);
175+
request.setAttribute(AuthenticatedRoleAttributeName, role);
176+
request.setAttribute(AuthenticatedDataAttributeName, authenticationDataSource);
177+
return true;
178+
}
179+
163180
@Override
164181
public AuthenticationState newAuthState(AuthData authData, SocketAddress remoteAddress, SSLSession sslSession)
165182
throws AuthenticationException {

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/authentication/AuthenticationService.java

+73-7
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.pulsar.broker.authentication;
2020

21+
import static org.apache.pulsar.broker.web.AuthenticationFilter.AuthenticatedDataAttributeName;
22+
import static org.apache.pulsar.broker.web.AuthenticationFilter.AuthenticatedRoleAttributeName;
2123
import java.io.Closeable;
2224
import java.io.IOException;
2325
import java.util.ArrayList;
@@ -28,10 +30,12 @@
2830
import java.util.stream.Collectors;
2931
import javax.naming.AuthenticationException;
3032
import javax.servlet.http.HttpServletRequest;
33+
import javax.servlet.http.HttpServletResponse;
3134
import org.apache.commons.lang3.StringUtils;
3235
import org.apache.pulsar.broker.PulsarServerException;
3336
import org.apache.pulsar.broker.ServiceConfiguration;
3437
import org.apache.pulsar.broker.web.AuthenticationFilter;
38+
import org.apache.pulsar.common.sasl.SaslConstants;
3539
import org.slf4j.Logger;
3640
import org.slf4j.LoggerFactory;
3741

@@ -85,16 +89,77 @@ public AuthenticationService(ServiceConfiguration conf) throws PulsarServerExcep
8589
}
8690
}
8791

92+
private String getAuthMethodName(HttpServletRequest request) {
93+
return request.getHeader(AuthenticationFilter.PULSAR_AUTH_METHOD_NAME);
94+
}
95+
96+
private AuthenticationProvider getAuthProvider(String authMethodName) throws AuthenticationException {
97+
AuthenticationProvider providerToUse = providers.get(authMethodName);
98+
if (providerToUse == null) {
99+
throw new AuthenticationException(
100+
String.format("Unsupported authentication method: [%s].", authMethodName));
101+
}
102+
return providerToUse;
103+
}
104+
105+
public boolean authenticateHttpRequest(HttpServletRequest request, HttpServletResponse response)
106+
throws Exception {
107+
String authMethodName = getAuthMethodName(request);
108+
if (authMethodName == null
109+
&& SaslConstants.SASL_TYPE_VALUE.equalsIgnoreCase(request.getHeader(SaslConstants.SASL_HEADER_TYPE))) {
110+
// This edge case must be handled because the Pulsar SASL implementation does not add the
111+
// X-Pulsar-Auth-Method-Name header.
112+
authMethodName = SaslConstants.AUTH_METHOD_NAME;
113+
}
114+
if (authMethodName != null) {
115+
AuthenticationProvider providerToUse = getAuthProvider(authMethodName);
116+
try {
117+
return providerToUse.authenticateHttpRequest(request, response);
118+
} catch (AuthenticationException e) {
119+
if (LOG.isDebugEnabled()) {
120+
LOG.debug("Authentication failed for provider " + providerToUse.getAuthMethodName() + " : "
121+
+ e.getMessage(), e);
122+
}
123+
throw e;
124+
}
125+
} else {
126+
for (AuthenticationProvider provider : providers.values()) {
127+
try {
128+
return provider.authenticateHttpRequest(request, response);
129+
} catch (AuthenticationException e) {
130+
if (LOG.isDebugEnabled()) {
131+
LOG.debug("Authentication failed for provider " + provider.getAuthMethodName() + ": "
132+
+ e.getMessage(), e);
133+
}
134+
// Ignore the exception because we don't know which authentication method is expected here.
135+
}
136+
}
137+
// No authentication provided
138+
if (!providers.isEmpty()) {
139+
if (StringUtils.isNotBlank(anonymousUserRole)) {
140+
request.setAttribute(AuthenticatedRoleAttributeName, anonymousUserRole);
141+
request.setAttribute(AuthenticatedDataAttributeName, new AuthenticationDataHttps(request));
142+
return true;
143+
}
144+
// If at least a provider was configured, then the authentication needs to be provider
145+
throw new AuthenticationException("Authentication required");
146+
} else {
147+
// No authentication required
148+
return true;
149+
}
150+
}
151+
}
152+
153+
/**
154+
* @deprecated use {@link #authenticateHttpRequest(HttpServletRequest, HttpServletResponse)}
155+
*/
156+
@Deprecated(since = "2.12.0")
88157
public String authenticateHttpRequest(HttpServletRequest request, AuthenticationDataSource authData)
89158
throws AuthenticationException {
90-
String authMethodName = request.getHeader(AuthenticationFilter.PULSAR_AUTH_METHOD_NAME);
159+
String authMethodName = getAuthMethodName(request);
91160

92161
if (authMethodName != null) {
93-
AuthenticationProvider providerToUse = providers.get(authMethodName);
94-
if (providerToUse == null) {
95-
throw new AuthenticationException(
96-
String.format("Unsupported authentication method: [%s].", authMethodName));
97-
}
162+
AuthenticationProvider providerToUse = getAuthProvider(authMethodName);
98163
try {
99164
if (authData == null) {
100165
AuthenticationState authenticationState = providerToUse.newHttpAuthState(request);
@@ -140,10 +205,11 @@ public String authenticateHttpRequest(HttpServletRequest request, Authentication
140205
/**
141206
* Mark this function as deprecated, it is recommended to use a method with the AuthenticationDataSource
142207
* signature to implement it.
208+
* @deprecated use {@link #authenticateHttpRequest(HttpServletRequest, HttpServletResponse)}.
143209
*/
144210
@Deprecated
145211
public String authenticateHttpRequest(HttpServletRequest request) throws AuthenticationException {
146-
return authenticateHttpRequest(request, null);
212+
return authenticateHttpRequest(request, (AuthenticationDataSource) null);
147213
}
148214

149215
public AuthenticationProvider getAuthenticationProvider(String authMethodName) {

pulsar-broker-common/src/main/java/org/apache/pulsar/broker/web/AuthenticationFilter.java

+1-47
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@
2828
import javax.servlet.ServletResponse;
2929
import javax.servlet.http.HttpServletRequest;
3030
import javax.servlet.http.HttpServletResponse;
31-
import org.apache.pulsar.broker.authentication.AuthenticationDataHttps;
3231
import org.apache.pulsar.broker.authentication.AuthenticationService;
33-
import org.apache.pulsar.broker.authentication.AuthenticationState;
34-
import org.apache.pulsar.common.sasl.SaslConstants;
3532
import org.slf4j.Logger;
3633
import org.slf4j.LoggerFactory;
3734

@@ -52,54 +49,12 @@ public AuthenticationFilter(AuthenticationService authenticationService) {
5249
this.authenticationService = authenticationService;
5350
}
5451

55-
private boolean isSaslRequest(HttpServletRequest request) {
56-
if (request.getHeader(SaslConstants.SASL_HEADER_TYPE) == null
57-
|| request.getHeader(SaslConstants.SASL_HEADER_TYPE).isEmpty()) {
58-
return false;
59-
}
60-
if (request.getHeader(SaslConstants.SASL_HEADER_TYPE)
61-
.equalsIgnoreCase(SaslConstants.SASL_TYPE_VALUE)) {
62-
return true;
63-
} else {
64-
return false;
65-
}
66-
}
67-
6852
@Override
6953
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
7054
throws IOException, ServletException {
7155
try {
72-
HttpServletRequest httpRequest = (HttpServletRequest) request;
73-
HttpServletResponse httpResponse = (HttpServletResponse) response;
74-
75-
if (!isSaslRequest(httpRequest)) {
76-
// not sasl type, return role directly.
77-
String authMethodName = httpRequest.getHeader(PULSAR_AUTH_METHOD_NAME);
78-
String role;
79-
if (authMethodName != null && authenticationService.getAuthenticationProvider(authMethodName) != null) {
80-
AuthenticationState authenticationState = authenticationService
81-
.getAuthenticationProvider(authMethodName).newHttpAuthState(httpRequest);
82-
request.setAttribute(AuthenticatedDataAttributeName, authenticationState.getAuthDataSource());
83-
role = authenticationService.authenticateHttpRequest(
84-
(HttpServletRequest) request, authenticationState.getAuthDataSource());
85-
} else {
86-
request.setAttribute(AuthenticatedDataAttributeName,
87-
new AuthenticationDataHttps((HttpServletRequest) request));
88-
role = authenticationService.authenticateHttpRequest((HttpServletRequest) request);
89-
}
90-
request.setAttribute(AuthenticatedRoleAttributeName, role);
91-
92-
if (LOG.isDebugEnabled()) {
93-
LOG.debug("[{}] Authenticated HTTP request with role {}", request.getRemoteAddr(), role);
94-
}
95-
chain.doFilter(request, response);
96-
return;
97-
}
98-
9956
boolean doFilter = authenticationService
100-
.getAuthenticationProvider(SaslConstants.AUTH_METHOD_NAME)
101-
.authenticateHttpRequest(httpRequest, httpResponse);
102-
57+
.authenticateHttpRequest((HttpServletRequest) request, (HttpServletResponse) response);
10358
if (doFilter) {
10459
chain.doFilter(request, response);
10560
}
@@ -111,7 +66,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
11166
} else {
11267
LOG.error("[{}] Error performing authentication for HTTP", request.getRemoteAddr(), e);
11368
}
114-
return;
11569
}
11670
}
11771

pulsar-broker-common/src/test/java/org/apache/pulsar/broker/authentication/AuthenticationProviderTokenTest.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -885,8 +885,8 @@ public void testTokenFromHttpParams() throws Exception {
885885
doReturn("127.0.0.1").when(servletRequest).getRemoteAddr();
886886
doReturn(0).when(servletRequest).getRemotePort();
887887

888-
AuthenticationState authState = provider.newHttpAuthState(servletRequest);
889-
provider.authenticate(authState.getAuthDataSource());
888+
boolean doFilter = provider.authenticateHttpRequest(servletRequest, null);
889+
assertTrue(doFilter, "Authentication should have passed");
890890
}
891891

892892
@Test
@@ -910,8 +910,8 @@ public void testTokenFromHttpHeaders() throws Exception {
910910
doReturn("127.0.0.1").when(servletRequest).getRemoteAddr();
911911
doReturn(0).when(servletRequest).getRemotePort();
912912

913-
AuthenticationState authState = provider.newHttpAuthState(servletRequest);
914-
provider.authenticate(authState.getAuthDataSource());
913+
boolean doFilter = provider.authenticateHttpRequest(servletRequest, null);
914+
assertTrue(doFilter, "Authentication should have passed");
915915
}
916916

917917
private static String createTokenWithAudience(Key signingKey, String audienceClaim, List<String> audience) {

0 commit comments

Comments
 (0)