strolch/web-rest/src/main/java/li/strolch/rest/filters/AuthenticationRequestFilter...

291 lines
10 KiB
Java

/*
* Copyright 2015 Robert von Burg <eitch@eitchnet.ch>
*
* Licensed 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 li.strolch.rest.filters;
import jakarta.annotation.Priority;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.*;
import jakarta.ws.rs.ext.Provider;
import li.strolch.exception.StrolchAccessDeniedException;
import li.strolch.exception.StrolchNotAuthenticatedException;
import li.strolch.privilege.model.Certificate;
import li.strolch.privilege.model.Usage;
import li.strolch.rest.RestfulStrolchComponent;
import li.strolch.rest.StrolchRestfulConstants;
import li.strolch.rest.helper.RestfulHelper;
import li.strolch.runtime.sessions.StrolchSessionHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static li.strolch.rest.StrolchRestfulConstants.*;
import static li.strolch.utils.helper.StringHelper.*;
/**
* This authentication request filter secures any requests to a Strolch server, by verifying that the request contains
* either the cookie {@link StrolchRestfulConstants#STROLCH_AUTHORIZATION} containing the authorization token, or the
* header {@link HttpHeaders#AUTHORIZATION} with the authorization token as its value.
*
* <br>
* <p>
* Sub classes should override {@link #validateSession(ContainerRequestContext, String)} to add further validation.
*
* @author Reto Breitenmoser <reto.breitenmoser@4trees.ch>
* @author Robert von Burg <eitch@eitchnet.ch>
*/
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationRequestFilter implements ContainerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationRequestFilter.class);
@Context
private HttpServletRequest request;
private Set<String> unsecuredPaths;
protected RestfulStrolchComponent getRestful() {
return RestfulStrolchComponent.getInstance();
}
protected StrolchSessionHandler getSessionHandler() {
return getRestful().getSessionHandler();
}
/**
* Defines the set of paths which are considered to be unsecured, i.e. can be requested without having logged in
* prior to the request
*
* @return the set of unsecured paths
*/
protected Set<String> getUnsecuredPaths() {
Set<String> paths = new HashSet<>();
paths.add("strolch/authentication");
paths.add("strolch/authentication/sso");
paths.add("strolch/version");
paths.add("strolch/languages");
return paths;
}
/**
* Validates if the path for the given request is for an unsecured path, i.e. no authorization is required
*
* @param requestContext the request context
*
* @return true if the request context is for an unsecured path, false if not, meaning authorization must be
* validated
*/
protected boolean isUnsecuredPath(ContainerRequestContext requestContext) {
// we have to allow OPTIONS for CORS
if (requestContext.getMethod().equals("OPTIONS"))
return true;
List<String> matchedURIs = requestContext.getUriInfo().getMatchedURIs();
// we allow unauthorized access to the authentication service
if (this.unsecuredPaths == null)
this.unsecuredPaths = getUnsecuredPaths();
return matchedURIs.stream().anyMatch(s -> this.unsecuredPaths.contains(s));
}
@Override
public void filter(ContainerRequestContext requestContext) {
String remoteIp = RestfulHelper.getRemoteIp(this.request);
logger.info("Remote IP: " + remoteIp + ": " + requestContext.getMethod() + " " + requestContext
.getUriInfo()
.getRequestUri());
try {
if (isUnsecuredPath(requestContext)) {
setCertificateIfAvailable(requestContext, remoteIp);
} else {
validateSession(requestContext, remoteIp);
}
} catch (StrolchNotAuthenticatedException e) {
logger.error(e.getMessage());
requestContext.abortWith(Response
.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("User is not authenticated!")
.build());
} catch (StrolchAccessDeniedException e) {
logger.error(e.getMessage());
requestContext.abortWith(Response
.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("User is not authorized!")
.build());
} catch (Exception e) {
logger.error(e.getMessage());
requestContext.abortWith(Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("User cannot access the resource.")
.build());
}
}
protected void setCertificateIfAvailable(ContainerRequestContext requestContext, String remoteIp) {
StrolchSessionHandler sessionHandler = getSessionHandler();
String sessionId = trimOrEmpty(requestContext.getHeaderString(HttpHeaders.AUTHORIZATION));
if (isNotEmpty(sessionId)) {
if (sessionHandler.isSessionKnown(sessionId)) {
validateCertificate(requestContext, sessionId, remoteIp);
} else {
logger.error("Session " + sessionId + " by authorization header does not exist anymore, ignoring!");
}
return;
}
sessionId = getSessionIdFromCookie(requestContext);
if (isEmpty(sessionId)) {
return;
}
if (sessionHandler.isSessionKnown(sessionId)) {
validateCertificate(requestContext, sessionId, remoteIp);
} else {
logger.error("Session " + sessionId + " by cookie does not exist anymore, ignoring!");
}
}
/**
* Validate the given request context by checking for the authorization cookie or header and then verifying a
* session exists and is valid with the given authoriation token
*
* <br>
* <p>
* Sub classes should override this method and first call super. If the return value is non-null, then further
* validation can be performed
*
* @param requestContext the request context for the secured path
* @param remoteIp the remote IP
*
* @return the certificate for the validated session, or null, of the request is aborted to no missing or invalid
* authorization token
*/
protected Certificate validateSession(ContainerRequestContext requestContext, String remoteIp) {
String authorization = trimOrEmpty(requestContext.getHeaderString(HttpHeaders.AUTHORIZATION));
if (authorization.isEmpty())
return validateCookie(requestContext, remoteIp);
if (authorization.startsWith("Basic "))
return authenticateBasic(requestContext, authorization, remoteIp);
return validateCertificate(requestContext, authorization, remoteIp);
}
protected String getSessionIdFromCookie(ContainerRequestContext requestContext) {
Cookie cookie = requestContext.getCookies().get(STROLCH_AUTHORIZATION);
if (cookie == null)
return "";
String sessionId = cookie.getValue();
if (sessionId == null)
return "";
return sessionId.trim();
}
protected Certificate validateCookie(ContainerRequestContext requestContext, String remoteIp) {
String sessionId = getSessionIdFromCookie(requestContext);
if (isEmpty(sessionId)) {
logger.error(
"No Authorization header or cookie on request to URL " + requestContext.getUriInfo().getPath());
requestContext.abortWith(Response
.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("Missing Authorization!")
.build());
return null;
}
return validateCertificate(requestContext, sessionId, remoteIp);
}
protected Certificate authenticateBasic(ContainerRequestContext requestContext, String authorization,
String remoteIp) {
if (!getRestful().isBasicAuthEnabled()) {
logger.error("Basic Auth is not available for URL " + requestContext.getUriInfo().getPath());
requestContext.abortWith(Response
.status(Response.Status.FORBIDDEN)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("Basic Auth not available")
.build());
return null;
}
String basicAuth = authorization.substring("Basic ".length());
basicAuth = new String(Base64.getDecoder().decode(basicAuth.getBytes()), StandardCharsets.UTF_8);
String[] parts = basicAuth.split(":");
if (parts.length != 2) {
requestContext.abortWith(Response
.status(Response.Status.BAD_REQUEST)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("Invalid Basic Authorization!")
.build());
return null;
}
logger.info("Performing basic auth for user " + parts[0] + "...");
StrolchSessionHandler sessionHandler = getSessionHandler();
Certificate certificate = sessionHandler.authenticate(parts[0], parts[1].toCharArray(), remoteIp, Usage.SINGLE,
false);
requestContext.setProperty(STROLCH_CERTIFICATE, certificate);
requestContext.setProperty(STROLCH_REQUEST_SOURCE, remoteIp);
return certificate;
}
protected Certificate validateCertificate(ContainerRequestContext requestContext, String sessionId,
String remoteIp) {
StrolchSessionHandler sessionHandler = getSessionHandler();
Certificate certificate = sessionHandler.validate(sessionId, remoteIp);
if (certificate.getUsage() == Usage.SET_PASSWORD) {
if (!requestContext
.getUriInfo()
.getMatchedURIs()
.contains("strolch/privilege/users/" + certificate.getUsername() + "/password")) {
requestContext.abortWith(Response
.status(Response.Status.FORBIDDEN)
.header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN)
.entity("Can only set password!")
.build());
return null;
}
}
requestContext.setProperty(STROLCH_CERTIFICATE, certificate);
requestContext.setProperty(STROLCH_REQUEST_SOURCE, remoteIp);
return certificate;
}
}