Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Determine Correct Expansion of AuthenticationProvider interface for WebSocket Proxy #20237

Open
michaeljmarshall opened this issue May 5, 2023 · 1 comment

Comments

@michaeljmarshall
Copy link
Member

Problem

The current AuthenticationProvider interface has the following method:

default boolean authenticateHttpRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
AuthenticationState authenticationState = newHttpAuthState(request);
String role = authenticateAsync(authenticationState.getAuthDataSource()).get();
request.setAttribute(AuthenticatedRoleAttributeName, role);
request.setAttribute(AuthenticatedDataAttributeName, authenticationState.getAuthDataSource());
return true;
} catch (AuthenticationException e) {
throw e;
} catch (Exception e) {
if (e instanceof ExecutionException && e.getCause() instanceof AuthenticationException) {
throw (AuthenticationException) e.getCause();
} else {
throw new AuthenticationException("Failed to authentication http request");
}
}
}

The HttpServletResponse appears to have been introduced for multi-stage http authentication, which is only used by the AuthenticationProviderSasl:

public boolean authenticateHttpRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
AuthenticationState state = getAuthState(request);
String saslAuthRoleToken = authRoleFromHttpRequest(request);
// role token exist
if (saslAuthRoleToken != null) {
// role token expired, send role token expired to client.
if (saslAuthRoleToken.equalsIgnoreCase(SASL_AUTH_ROLE_TOKEN_EXPIRED)) {
setResponseHeaderState(response, SASL_AUTH_ROLE_TOKEN_EXPIRED);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Role token expired");
if (log.isDebugEnabled()) {
log.debug("[{}] Server side role token expired: {}", request.getRequestURI(), saslAuthRoleToken);
}
return false;
}
// role token OK to use,
// if request is ask for role token verify, send auth complete to client
// if request is a real request with valid role token, pass this request down.
if (request.getHeader(SASL_HEADER_STATE).equalsIgnoreCase(SASL_STATE_COMPLETE)) {
request.setAttribute(AuthenticatedRoleAttributeName, saslAuthRoleToken);
request.setAttribute(AuthenticatedDataAttributeName,
new AuthenticationDataHttps(request));
if (log.isDebugEnabled()) {
log.debug("[{}] Server side role token OK to go on: {}", request.getRequestURI(),
saslAuthRoleToken);
}
return true;
} else {
checkState(request.getHeader(SASL_HEADER_STATE).equalsIgnoreCase(SASL_STATE_SERVER_CHECK_TOKEN));
setResponseHeaderState(response, SASL_STATE_COMPLETE);
response.setHeader(SASL_STATE_SERVER, request.getHeader(SASL_STATE_SERVER));
response.setStatus(HttpServletResponse.SC_OK);
if (log.isDebugEnabled()) {
log.debug("[{}] Server side role token verified success: {}", request.getRequestURI(),
saslAuthRoleToken);
}
return false;
}
} else {
// no role token, do sasl auth
// need new authState
if (state == null || request.getHeader(SASL_HEADER_STATE).equalsIgnoreCase(SASL_STATE_CLIENT_INIT)) {
state = newAuthState(null, null, null);
authStates.put(state.getStateId(), state);
}
checkState(request.getHeader(SASL_AUTH_TOKEN) != null,
"Header token should exist if no role token.");
// do the sasl auth
AuthData clientData = AuthData.of(Base64.getDecoder().decode(
request.getHeader(SASL_AUTH_TOKEN)));
AuthData brokerData = state.authenticate(clientData);
// authentication has completed, it has get the auth role.
if (state.isComplete()) {
if (log.isDebugEnabled()) {
log.debug("[{}] SASL server authentication complete, send OK to client.", request.getRequestURI());
}
String authRole = state.getAuthRole();
String authToken = createAuthRoleToken(authRole, String.valueOf(state.getStateId()));
response.setHeader(SASL_AUTH_ROLE_TOKEN, authToken);
// auth request complete, return OK, wait for a new real request to come.
response.setHeader(SASL_STATE_SERVER, String.valueOf(state.getStateId()));
setResponseHeaderState(response, SASL_STATE_COMPLETE);
response.setStatus(HttpServletResponse.SC_OK);
// auth completed, no need to keep authState
authStates.remove(state.getStateId());
return false;
} else {
// auth not complete
if (log.isDebugEnabled()) {
log.debug("[{}] SASL server authentication not complete, send {} back to client.",
request.getRequestURI(), HttpServletResponse.SC_UNAUTHORIZED);
}
setResponseHeaderState(response, SASL_STATE_NEGOTIATE);
response.setHeader(SASL_STATE_SERVER, String.valueOf(state.getStateId()));
response.setHeader(SASL_AUTH_TOKEN, Base64.getEncoder().encodeToString(brokerData.getBytes()));
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "SASL Authentication not complete.");
return false;
}
}
}

However, the WebSocket Proxy does not have access to an HttpServletResponse. It only has access to a ServletUpgradeResponse, as seen here:

protected boolean checkAuth(ServletUpgradeResponse response) {
String authRole = "<none>";
String authMethodName = request.getHeader(PULSAR_AUTH_METHOD_NAME);
AuthenticationState authenticationState = null;
if (service.isAuthenticationEnabled()) {
try {
if (authMethodName != null
&& service.getAuthenticationService().getAuthenticationProvider(authMethodName) != null) {
authenticationState = service.getAuthenticationService()
.getAuthenticationProvider(authMethodName).newHttpAuthState(request);
}
if (authenticationState != null) {
authRole = service.getAuthenticationService()
.authenticateHttpRequest(request, authenticationState.getAuthDataSource());
} else {
authRole = service.getAuthenticationService().authenticateHttpRequest(request);
}
log.info("[{}:{}] Authenticated WebSocket client {} on topic {}", request.getRemoteAddr(),
request.getRemotePort(), authRole, topic);
} catch (javax.naming.AuthenticationException e) {
log.warn("[{}:{}] Failed to authenticated WebSocket client {} on topic {}: {}", request.getRemoteAddr(),
request.getRemotePort(), authRole, topic, e.getMessage());
try {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Failed to authenticate");
} catch (IOException e1) {
log.warn("[{}:{}] Failed to send error: {}", request.getRemoteAddr(), request.getRemotePort(),
e1.getMessage(), e1);
}
return false;
}
}

Therefore, the current API does not allow for multi-phased http authentication.

Observations

ServletUpgradeResponse is a wrapper for HttpServletResponse, but it doesn't provide direct access. Does that mean we should add a new method to the AuthenticationProvider interface just for the WebSocket?

For now, the current state is that multi-stage auth is not supported in the WebSocket proxy. This technically aligns with the current limitation that multi-stage auth is not available in the regular pulsar proxy #19291.

michaeljmarshall added a commit that referenced this issue May 8, 2023
Fixes #20236 
PIP: #19409 

### Motivation

In the `AuthenticationService`, we are currently using the deprecated `authenticate` methods. As a result, we hit the `Not Implemented` exception when using the `AuthenticationProviderOpenID`. This PR updates the implementation so that we're able 

This solution isn't ideal for two reasons.

1. We are not using the `authenticationHttpRequest` method, which seems like the right method for the WebSocket proxy. However, this is not a viable option, as I documented in #20237.
2. We are calling `.get()` on a future. However, it is expected that the `AuthenticationProvider` not block forever, so I think this is acceptable for now. Please let me know if you disagree.

### Modifications

* Replace `authenticate` with `authenticateAsync`.

### Verifying this change

This change is a trivial rework / code cleanup without any test coverage.

### Documentation

- [x] `doc-not-needed`

Note that I do have documentation showing that 3.0.x does not support OIDC in the WebSocket Proxy. The `next` docs don't need that limitation since this PR fixes that and targets 3.1.0. apache/pulsar-site#558

### Matching PR in forked repository

PR in forked repository: skipping for this trivial PR
michaeljmarshall added a commit to datastax/pulsar that referenced this issue May 8, 2023
Fixes apache#20236
PIP: apache#19409

### Motivation

In the `AuthenticationService`, we are currently using the deprecated `authenticate` methods. As a result, we hit the `Not Implemented` exception when using the `AuthenticationProviderOpenID`. This PR updates the implementation so that we're able

This solution isn't ideal for two reasons.

1. We are not using the `authenticationHttpRequest` method, which seems like the right method for the WebSocket proxy. However, this is not a viable option, as I documented in apache#20237.
2. We are calling `.get()` on a future. However, it is expected that the `AuthenticationProvider` not block forever, so I think this is acceptable for now. Please let me know if you disagree.

### Modifications

* Replace `authenticate` with `authenticateAsync`.

### Verifying this change

This change is a trivial rework / code cleanup without any test coverage.

### Documentation

- [x] `doc-not-needed`

Note that I do have documentation showing that 3.0.x does not support OIDC in the WebSocket Proxy. The `next` docs don't need that limitation since this PR fixes that and targets 3.1.0. apache/pulsar-site#558

### Matching PR in forked repository

PR in forked repository: skipping for this trivial PR

(cherry picked from commit 03dc3db)
@github-actions
Copy link

github-actions bot commented Jun 5, 2023

The issue had no activity for 30 days, mark with Stale label.

@github-actions github-actions bot added the Stale label Jun 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant